aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2011-05-09 17:19:26 +0200
committerAnton Khirnov <anton@khirnov.net>2012-01-14 08:39:19 +0100
commit6bc2f5a999d9ff77779d98b7cc688a4350ea65d9 (patch)
treed1f43c138aafb0e1fb05e403dc6fa5b7b442ccc6
parent647c2509897e8a7cf12dba1a6ee81f8bb6e53b4d (diff)
Add a python rewrite of the vim plugin.
-rw-r--r--vim/plugin/nm_vim.py671
-rw-r--r--vim/plugin/notmuch-vimpy.vim863
-rw-r--r--vim/syntax/nm_vimpy-compose.vim7
-rw-r--r--vim/syntax/nm_vimpy-folders.vim15
-rw-r--r--vim/syntax/nm_vimpy-search.vim18
-rw-r--r--vim/syntax/nm_vimpy-show.vim23
6 files changed, 1597 insertions, 0 deletions
diff --git a/vim/plugin/nm_vim.py b/vim/plugin/nm_vim.py
new file mode 100644
index 0000000..ef1dfb3
--- /dev/null
+++ b/vim/plugin/nm_vim.py
@@ -0,0 +1,671 @@
+#
+# This file is part of Notmuch.
+#
+# Notmuch 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.
+#
+# Notmuch 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 Notmuch. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# python-notmuch wrapper for the notmuch vim client
+
+import codecs
+import datetime
+import email, email.header, email.charset, email.utils, email.message
+import email.mime.text, email.mime.audio, email.mime.image, email.mime.multipart, email.mime.application
+import mailbox
+import mailcap
+import mimetypes
+import notmuch
+import os, os.path
+import shlex
+import smtplib
+import subprocess
+import tempfile
+import vim
+
+#### classes ####
+
+class NMBuffer(object):
+ """
+ An object mapping line ranges in current buffer to notmuch structures.
+ """
+
+ # a string identifying the buffer
+ id = None
+
+ # a list of NMBufferElement subclasses representing objects in current buffer
+ objects = None
+
+ def __new__(cls, *args, **kwargs):
+ bufnr = vim.eval('bufnr("%")')
+ if bufnr in nm_buffers:
+ return nm_buffers[bufnr]
+ ret = object.__new__(cls)
+ nm_buffers[bufnr] = ret
+ return ret
+
+ def __init__(self):
+ self.objects = []
+
+ def get_object(self, line, offset):
+ """
+ Get an object that's offset objects away from given line or None.
+ E.g. offset = 0 gets the object on the line, offset = 1 gets
+ next, offset = -1 previous.
+ """
+ if not self.objects:
+ return None
+
+ if offset > 0 and line < self.objects[0].start:
+ return self.objects[min(len(self.objects), offset) - 1]
+ if offset < 0 and line > self.objects[-1].end:
+ return self.objects[-min(len(self.objects), offset)]
+
+ for i in xrange(len(self.objects)):
+ obj = self.objects[i]
+ if line >= obj.start and line <= obj.end:
+ return self.objects[ max(min(len(self.objects) - 1, i + offset), 0) ]
+
+ def tag(self, tags, querystr = None):
+ if querystr:
+ querystr = '( %s ) and ( %s )'%(self.id, querystr)
+ else:
+ querystr = self.id
+
+ db = notmuch.Database(mode = notmuch.Database.MODE.READ_WRITE)
+ map_query(db, querystr, lambda m, l: self._tag_message(m, tags))
+
+ def _tag_message(self, message, tags):
+ for tag in tags:
+ if tag[0] == '+':
+ message.add_tag(tag[1:])
+ elif tag[0] == '-':
+ message.remove_tag(tag[1:])
+
+class SavedSearches(NMBuffer):
+ """
+ This buffer displays a list of saved searches ('folders').
+ """
+
+ def __init__(self, folders):
+ """
+ @param folders A list of (folder name, query string) tuples.
+ """
+ super(SavedSearches, self).__init__()
+ for folder in folders:
+ self.objects.append(Folder(0, 0, folder[1], folder[0]))
+ self.refresh()
+
+ def refresh(self):
+ b = vim.current.buffer
+ db = notmuch.Database()
+ for obj in self.objects:
+ q = db.create_query(obj.id)
+ q1 = db.create_query('( %s ) and tag:unread'%(obj.id))
+ b.append('{0:>7} {1:7} {2: <30s} ({3})'.format(q.count_messages(), '({0})'.format(q1.count_messages()),
+ obj.name, obj.id))
+ obj.start = obj.end = len(b) - 1
+
+ def __repr__(self):
+ return '<Vim-notmuch saved searches buffer.>'
+
+ def tag(self):
+ raise TypeError('Attempted to tag in folders view.')
+
+class Search(NMBuffer):
+ """
+ This buffer displays results of a db search -- a list of found threads.
+ """
+
+ def __init__(self, querystr = '', parent = None):
+ super(Search, self).__init__()
+
+ assert(querystr or parent)
+
+ # FIXME simplify
+ if parent:
+ self.id = parent.id
+ if querystr:
+ if self.id:
+ self.id = '( %s ) and ( %s )'%(self.id, querystr)
+ else:
+ self.id = querystr
+
+ self.refresh()
+
+ def refresh(self):
+ self.objects = []
+ b = vim.current.buffer
+ db = notmuch.Database()
+ q = db.create_query(self.id)
+
+ q.set_sort(q.SORT.NEWEST_FIRST) # FIXME allow different search orders
+ for t in q.search_threads():
+ start = len(b)
+ datestr = get_relative_date(t.get_newest_date())
+ authors = t.get_authors()
+
+ # notmuch returns different thread subjects depending on
+ # sort order
+ # we assume that 'thread subject' == 'subject of the first mail in the thread'
+ subj = None
+ for m in t.get_toplevel_messages():
+ subj = m.get_header('subject')
+ break
+ if not subj:
+ subj = t.get_subject()
+
+ tags = str(t.get_tags())
+ b.append((u'%-12s %3s/%3s %-20.20s | %s (%s)'%(datestr, t.get_matched_messages(), t.get_total_messages(),
+ authors, subj, tags)).encode('utf-8'))
+ self.objects.append(NMBufferElement(start, len(b) - 1, t.get_thread_id()))
+
+ def __repr__(self):
+ return '<Vim-notmuch search buffer: %s>'%(self.id)
+
+class ShowThread(NMBuffer):
+ """
+ This buffer represents a thread view.
+ """
+
+ # a list of temporary files for viewing attachments
+ # they will be automagically closed and deleted on this object's demise
+ _tmpfiles = None
+
+ # a list of headers to show
+ _headers = None
+
+ def __init__(self, querystr):
+ self.id = querystr
+ self._tmpfiles = []
+ self.refresh()
+
+ def refresh(self):
+ self._headers = vim.eval('g:nm_vimpy_show_headers')
+ self.objects = []
+ db = notmuch.Database()
+ map_query(db, self.id, self._print_message)
+
+ def _get_attachment(self, line):
+ message = self.get_object(line, 0)
+ if not message:
+ print 'No message on this line.'
+ return None
+
+ data = None
+ for a in message.attachments:
+ if line == a.start:
+ return a.data
+ print 'No attachment on this line'
+
+ def view_attachment(self, line):
+ """
+ View attachment corresponding to given line.
+ """
+ data = self._get_attachment(line)
+ if not data:
+ return
+
+ f = tempfile.NamedTemporaryFile()
+ f.write(data.get_payload(decode = True))
+ f.flush()
+ os.fsync(f.fileno())
+
+ caps = mailcap.getcaps()
+ ret = mailcap.findmatch(caps, data.get_content_type(), filename = f.name)
+ if ret[0]:
+ with open(os.devnull, 'w') as null:
+ subprocess.Popen(shlex.split(ret[0]), stderr = null, stdout = null)
+ self._tmpfiles.append(f)
+
+ def save_attachment(self, line, filename):
+ """
+ Save attachment corresponding to given line to the specified file.
+ """
+ data = self._get_attachment(line)
+ if not data:
+ return
+
+ if os.path.isdir(filename):
+ filename = os.path.join(filename, data.get_filename())
+
+ with open(os.path.expanduser(filename), 'w') as f:
+ f.write(data.get_payload(decode = True))
+
+ def _read_text_payload(self, part):
+ """
+ Try converting the payload of the MIME part into utf-8.
+ """
+ p = part.get_payload(decode = True)
+ ch = part.get_content_charset('utf-8')
+ try:
+ codec = codecs.lookup(ch)
+ except LookupError:
+ print 'Unknown encoding: %s'%ch
+ return p
+
+ if codec.name != 'utf-8':
+ try:
+ p = p.decode(codec.name).encode('utf-8')
+ except UnicodeDecodeError:
+ return 'Error decoding the message.'
+ return p
+
+ def _print_part(self, part):
+ """
+ Walk through the part recursively and print it
+ and its subparts to current buffer.
+ """
+ if part.is_multipart():
+ if part.get_content_subtype() == 'alternative':
+ # try to find the plaintext version, if that fails just try to print the first
+ for subpart in part.get_payload():
+ if subpart.get_content_type() == 'text/plain':
+ return self._print_part(subpart)
+ self._print_part(part.get_payload()[0])
+ else:
+ for subpart in part.get_payload():
+ self._print_part(subpart)
+ else:
+ b = vim.current.buffer
+ if part.get('Content-Disposition', '').lower().startswith('attachment'):
+ self.objects[-1].attachments.append(Attachment(len(b), len(b), part))
+ b.append(('[ Attachment: %s (%s)]'%(part.get_filename(), part.get_content_type())).split('\n'))
+
+ if part.get_content_maintype() == 'text':
+ p = self._read_text_payload(part)
+ b.append(p.split('\n'))
+
+ def _print_message(self, message, level):
+ b = vim.current.buffer
+ msg = Message(len(b), 0, message.get_message_id())
+ self.objects.append(msg)
+
+ msg.tags = list(message.get_tags())
+
+ fp = open(message.get_filename())
+ email_msg = email.message_from_file(fp)
+ fp.close()
+
+ # print the title
+ b.append('%d/'%level + 20*'-' + 'message start' + 20*'-' + '\\')
+
+ name, addr = email.utils.parseaddr(message.get_header('from'))
+ author = name if name else addr
+ titlestr = '%s: %s (%s) (%s)'%(author, message.get_header('subject'), get_relative_date(message.get_date()),
+ ' '.join(message.get_tags()))
+ b.append(titlestr.encode('utf-8'))
+
+ # print the headers
+ for header in self._headers:
+ if header in email_msg:
+ b.append(('%s: %s'%(header, decode_header(email_msg[header]))).split('\n'))
+ b.append('')
+
+ self._print_part(email_msg)
+
+ b.append('\\' + 20*'-' + 'message end' + 20*'-' + '/')
+ msg.end = len(b) - 1
+
+ def __repr__(self):
+ return '<Vim-notmuch thread buffer: %s.>'%self.id
+
+ def get_message_for_tag(self, tag):
+ """
+ Return the first message for given tag.
+ """
+ for msg in self.objects:
+ if tag in msg.tags:
+ return msg
+ return None
+
+class Compose(NMBuffer):
+
+ _attachment_prefix = 'Notmuch-Attachment: '
+
+ def __init__(self):
+ super(Compose, self).__init__()
+ # python wants to use base64 for some reason, force quoted-printable
+ email.charset.add_charset('utf-8', email.charset.QP, email.charset.QP, 'utf-8')
+
+ def _encode_header(self, text):
+ try:
+ text.decode('ascii')
+ return text
+ except UnicodeDecodeError:
+ return email.header.Header(text, 'utf-8').encode()
+
+ def attach(self, filename):
+ type, encoding = mimetypes.guess_type(filename)
+ if encoding or not type:
+ type = 'application/octet-stream'
+ vim.current.buffer.append('%s%s:%s'%(self._attachment_prefix, filename, type), 0)
+
+ def send(self):
+ """
+ Send the message in current buffer.
+ """
+ b = vim.current.buffer
+ i = 0
+
+ # parse attachments
+ attachments = []
+ while b[i].startswith(self._attachment_prefix):
+ filename, sep, type = b[i][len(self._attachment_prefix):].rpartition(':')
+ attachments.append((os.path.expanduser(filename), type))
+ i += 1
+ # skip the inline help
+ while b[i].startswith('Notmuch-Help:'):
+ i += 1
+
+ # add the headers
+ headers = {}
+ recipients = []
+ from_addr = None
+ while i < len(b):
+ if not b[i]:
+ break
+ key, sep, val = b[i].partition(':')
+ i += 1
+
+ try:
+ key.decode('ascii')
+ except UnicodeDecodeError:
+ raise ValueError('Header name must be ASCII only.')
+
+ if not val.strip():
+ # skip empty headers
+ continue
+
+ if key.lower() in ('to', 'cc', 'bcc'):
+ names, addrs = zip(*email.utils.getaddresses([val]))
+ names = map(self._encode_header, names)
+
+ recipients += addrs
+ if key.lower() == 'bcc':
+ continue
+ val = ','.join(map(email.utils.formataddr, zip(names, addrs)))
+ else:
+ if key.lower() == 'from':
+ from_addr = email.utils.parseaddr(val)[1]
+ val = self._encode_header(val)
+
+ headers[key] = val
+
+ body = email.mime.text.MIMEText('\n'.join(b[i:]), 'plain', 'utf-8')
+ # add the body
+ if not attachments:
+ msg = body
+ else:
+ msg = email.mime.multipart.MIMEMultipart()
+ msg.attach(body)
+ for attachment in attachments:
+ maintype, subtype = attachment[1].split('/', 1)
+
+ if maintype == 'text':
+ with open(attachment[0]) as f:
+ part = email.mime.text.MIMEText(f.read(), subtype, 'utf-8')
+ else:
+ if maintype == 'image':
+ obj = email.mime.image.MIMEImage
+ elif maintype == 'audio':
+ obj = email.mime.audio.MIMEAudio
+ else:
+ obj = email.mime.application.MIMEApplication
+
+ with open(attachment[0]) as f:
+ part = obj(f.read(), subtype)
+
+ part.add_header('Content-Disposition', 'attachment',
+ filename = os.path.basename(attachment[0]))
+ msg.attach(part)
+
+ msg['User-Agent'] = vim.eval('g:nm_vimpy_user_agent')
+ msg['Message-ID'] = email.utils.make_msgid()
+ msg['Date'] = email.utils.formatdate(localtime = True)
+ for key in headers:
+ msg[key] = headers[key]
+
+ # sanity checks
+ if not from_addr:
+ # XXX notmuch-python should export the user email address
+ raise ValueError('No sender address specified.')
+ if not recipients:
+ raise ValueError('No recipient specified.')
+
+ # send
+ fcc = vim.eval('g:nm_vimpy_fcc_maildir')
+ if fcc:
+ dbroot = notmuch.Database().get_path()
+ mdir = mailbox.Maildir(os.path.join(dbroot, fcc))
+ mdir.add(msg)
+ mdir.close()
+ # TODO configurable host
+ s = smtplib.SMTP('localhost')
+ ret = s.sendmail(from_addr, recipients, msg.as_string())
+ for key in ret:
+ print 'Error sending mail to %s: %s'%(key, ret[key])
+ s.quit()
+
+class RawMessage(NMBuffer):
+
+ def __init__(self, msgid):
+ db = notmuch.Database()
+ msg = db.find_message(msgid)
+
+ if msg:
+ with open(msg.get_filename()) as fp:
+ vim.current.buffer.append(fp.readlines())
+
+class NMBufferElement(object):
+ """
+ This object represents a structure (e.g. folder, thread or message)
+ corresponding to a range of lines in NMBuffer.
+ """
+ # start and end lines, 0-based
+ start = None
+ end = None
+ # a string identifying the element
+ id = None
+
+ def __init__(self, start, end, id):
+ self.start = start
+ self.end = end
+ self.id = id
+
+class Folder(NMBufferElement):
+ """
+ A saved search / 'folder'.
+ """
+ # folder name
+ name = None
+
+ def __init__(self, start, end, id, name):
+ super(Folder, self).__init__(start, end, id)
+ self.name = name
+
+class Message(NMBufferElement):
+
+ attachments = None
+ tags = None
+
+ def __init__(self, start, end, id):
+ super(Message, self).__init__(start, end, id)
+ self.attachments = []
+
+class Attachment(NMBufferElement):
+ data = None
+
+ def __init__(self, start, end, data):
+ super(Attachment, self).__init__(start, end, '')
+ self. data = data
+
+#### global variables ####
+# this dictionary stores the Python objects corresponding to notmuch-managed
+# buffers, each indexed by the buffer number
+nm_buffers = {}
+
+#### utility functions ####
+
+def get_current_buffer():
+ """
+ Get the NMBuffer object associated with current buffer or None.
+ """
+ try:
+ return nm_buffers[vim.eval('bufnr("%")')]
+ except KeyError:
+ return None
+
+def delete_current_buffer():
+ """
+ Delete the NMBuffer associated with current buffer.
+ """
+ del nm_buffers[vim.eval('bufnr("%")')]
+
+def get_relative_date(timestamp):
+ """
+ Format a nice representation of 'time' relative to the current time.
+
+ Examples include:
+
+ 5 mins. ago (For times less than 60 minutes ago)
+ Today 12:30 (For times >60 minutes but still today)
+ Yest. 12:30
+ Mon. 12:30 (Before yesterday but fewer than 7 days ago)
+ October 12 (Between 7 and 180 days ago (about 6 months))
+ 2008-06-30 (More than 180 days ago)
+
+ Shamelessly lifted from notmuch-time
+ TODO: this should probably be in python-notmuch
+ """
+ try:
+ now = datetime.datetime.now()
+ then = datetime.datetime.fromtimestamp(timestamp)
+ except ValueError:
+ return 'when?'
+
+
+ if then > now:
+ return 'the future'
+
+ delta = now - then
+
+ if delta.days > 180:
+ return then.strftime("%F") # 2008-06-30
+
+ total_seconds = delta.seconds + delta.days * 24 * 3600
+ if total_seconds < 3600:
+ return "%d min. ago"%(total_seconds / 60)
+
+ if delta.days < 7:
+ if then.day == now.day:
+ return then.strftime("Today %R") # Today 12:30
+ if then.day + 1 == now.day:
+ return then.strftime("Yest. %R") # Yest. 12:30
+ return then.strftime("%a. %R") # Mon. 12:30
+
+ return then.strftime("%B %d") # October 12
+
+def map_query(db, query, run):
+ """
+ Execute runnable run on every message found for the specified query.
+ """
+ def walk_query(message, run, level):
+ run(message, level)
+ level += 1
+ replies = message.get_replies()
+ if replies is not None:
+ for reply in replies:
+ walk_query(reply, run, level)
+
+ q = db.create_query(query)
+ for t in q.search_threads():
+ for m in t.get_toplevel_messages():
+ walk_query(m, run, 0)
+
+def decode_header(header):
+ """
+ Decode a RFC 2822 header into a utf-8 string.
+ """
+ ret = []
+ for part in email.header.decode_header(header):
+ if part[1]:
+ try:
+ ret.append(part[0].decode(part[1]).encode('utf-8'))
+ continue
+ except LookupError:
+ print 'Unknown encoding: %s'%part[1]
+ ret.append(part[0])
+
+ return ' '.join(ret)
+
+#### functions for exporting stuff to viml ####
+
+def vim_get_tags():
+ """
+ Export a string listing all tags in the database, one per line into
+ a vim variable named 'taglist'.
+ """
+ db = notmuch.Database()
+ tags = '\n'.join(db.get_all_tags())
+ vim.command('let taglist = \'%s\''%tags)
+
+def vim_get_object(line, offset):
+ """
+ Export start/end lines and id of the object that's offset objects
+ aways from given line into a dict named 'obj' in viml.
+ E.g. offset = 0 gets the object on the line, offset = 1 gets
+ next, offset = -1 previous.
+ This method is a noop if current line doesn't correspond to anything.
+ """
+ # vim lines are 1-based
+ line -= 1
+ assert line >= 0
+
+ obj = get_current_buffer().get_object(line, offset)
+ if obj:
+ vim.command('let obj = { "start" : %d, "end" : %d, "id" : "%s" }'%(
+ obj.start + 1, obj.end + 1, obj.id))
+
+def vim_get_id():
+ """
+ Export the id string of current buffer into a vim string named 'buf_id'.
+ """
+ id = get_current_buffer().id
+ vim.command('let buf_id = "%s"'%(id if id else ''))
+
+def vim_view_attachment(line):
+ """
+ View an attachment corresponding to the given vim line
+ in an external viewer.
+ """
+ line -= 1 # vim lines are 1-based
+ get_current_buffer().view_attachment(line)
+def vim_save_attachment(line, filename):
+ """
+ Save an attachment corresponding to the given vim line into given file.
+ """
+ line -= 1 # vim lines are 1-based
+ get_current_buffer().save_attachment(line, filename)
+
+def vim_get_message_for_tag(tag):
+ """
+ Export start line for the first message with the given tag into
+ a vim variable named 'start'.
+ """
+ msg = get_current_buffer().get_message_for_tag(tag)
+ if msg:
+ start = msg.start + 1
+ else:
+ start = 0
+
+ vim.command('let start = %d'%start)
diff --git a/vim/plugin/notmuch-vimpy.vim b/vim/plugin/notmuch-vimpy.vim
new file mode 100644
index 0000000..8d80a0e
--- /dev/null
+++ b/vim/plugin/notmuch-vimpy.vim
@@ -0,0 +1,863 @@
+" nm-vimpy.vim plugin --- run notmuch within vim
+"
+" This file is part of Notmuch.
+"
+" Notmuch 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.
+"
+" Notmuch 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 Notmuch. If not, see <http://www.gnu.org/licenses/>.
+"
+" Based on the notmuch.vim plugin by:
+" Authors: Bart Trojanowski <bart@jukie.net>
+" Contributors: Felipe Contreras <felipe.contreras@gmail.com>,
+" Peter Hartman <peterjohnhartman@gmail.com>
+"
+" Mostly rewritten using python by Anton Khirnov <anton@khirnov.net>
+"
+
+
+" --- initialization
+if exists('s:nm_vimpy_loaded') || &cp
+ finish
+endif
+
+" init the python layer
+let s:python_path = expand('<sfile>:p:h')
+python import sys
+exec "python sys.path += [r'" . s:python_path . "']"
+python import vim, nm_vim
+
+command! NMVimpy call NMVimpy()
+
+" --- configuration defaults
+
+let s:nm_vimpy_defaults = {
+ \ 'g:notmuch_cmd': 'notmuch' ,
+ \
+ \ 'g:notmuch_search_newest_first': 1 ,
+ \
+ \ 'g:notmuch_compose_insert_mode_start': 1 ,
+ \ 'g:notmuch_compose_header_help': 1 ,
+ \ 'g:notmuch_compose_temp_file_dir': '~/.notmuch/compose/' ,
+ \ 'g:nm_vimpy_fcc_maildir': 'sent' ,
+ \ 'g:nm_vimpy_user_agent': 'notmuch-vimpy' ,
+ \ }
+
+" defaults for g:nm_vimpy_folders
+" override with: let g:nm_vimpy_folders = [ ... ]
+let s:nm_vimpy_folders_defaults = [
+ \ [ 'new', 'tag:inbox and tag:unread' ],
+ \ [ 'inbox', 'tag:inbox' ],
+ \ [ 'unread', 'tag:unread' ],
+ \ ]
+
+let s:nm_vimpy_show_headers_defaults = [
+ \ 'From',
+ \ 'To',
+ \ 'Cc',
+ \ 'Subject',
+ \ 'Date',
+ \ 'Reply-To',
+ \ 'Message-Id',
+ \]
+
+" defaults for g:nm_vimpy_compose_headers
+" override with: let g:nm_vimpy_compose_headers = [ ... ]
+let s:nm_vimpy_compose_headers_defaults = [
+ \ 'From',
+ \ 'To',
+ \ 'Cc',
+ \ 'Bcc',
+ \ 'Subject'
+ \ ]
+
+" --- keyboard mapping definitions
+
+" --- --- bindings for folders mode {{{2
+
+let g:nm_vimpy_folders_maps = {
+ \ 'm': ':call <SID>NM_new_mail()<CR>',
+ \ 's': ':call <SID>NM_search_prompt(0)<CR>',
+ \ 'q': ':call <SID>NM_kill_this_buffer()<CR>',
+ \ '=': ':call <SID>NM_folders_refresh_view()<CR>',
+ \ '<Enter>': ':call <SID>NM_folders_show_search('''')<CR>',
+ \ '<Space>': ':call <SID>NM_folders_show_search(''tag:unread'')<CR>',
+ \ 'tt': ':call <SID>NM_folders_from_tags()<CR>',
+ \ }
+
+" --- --- bindings for search screen {{{2
+let g:nm_vimpy_search_maps = {
+ \ '<Enter>': ':call <SID>NM_search_show_thread()<CR>',
+ \ '<Space>': ':call <SID>NM_search_show_thread_unread()<CR>',
+ \ '<C-]>': ':call <SID>NM_search_expand(''<cword>'')<CR>',
+ \ 'a': ':call <SID>NM_search_archive_thread()<CR>',
+ \ 'A': ':call <SID>NM_search_mark_read_then_archive_thread()<CR>',
+ \ 'D': ':call <SID>NM_search_delete_thread()<CR>',
+ \ 'f': ':call <SID>NM_search_filter()<CR>',
+ \ 'm': ':call <SID>NM_new_mail()<CR>',
+ \ 'o': ':call <SID>NM_search_toggle_order()<CR>',
+ \ 'r': ':call <SID>NM_search_reply_to_thread()<CR>',
+ \ 's': ':call <SID>NM_search_prompt(0)<CR>',
+ \ ',s': ':call <SID>NM_search_prompt(1)<CR>',
+ \ 'q': ':call <SID>NM_kill_this_buffer()<CR>',
+ \ '+': ':call <SID>NM_search_add_tags([])<CR>',
+ \ '-': ':call <SID>NM_search_remove_tags([])<CR>',
+ \ '=': ':call <SID>NM_search_refresh_view()<CR>',
+ \ }
+
+" --- --- bindings for show screen {{{2
+let g:nm_vimpy_show_maps = {
+ \ '<C-P>': ':call <SID>NM_jump_message(-1)<CR>',
+ \ '<C-N>': ':call <SID>NM_jump_message(+1)<CR>',
+ \ '<C-]>': ':call <SID>NM_search_expand(''<cword>'')<CR>',
+ \ 'q': ':call <SID>NM_kill_this_buffer()<CR>',
+ \ 's': ':call <SID>NM_search_prompt(0)<CR>',
+ \
+ \
+ \ 'a': ':call <SID>NM_show_archive_thread()<CR>',
+ \ 'A': ':call <SID>NM_show_mark_read_then_archive_thread()<CR>',
+ \ 'N': ':call <SID>NM_show_mark_read_then_next_open_message()<CR>',
+ \ 'v': ':call <SID>NM_show_view_all_mime_parts()<CR>',
+ \ '+': ':call <SID>NM_show_add_tag()<CR>',
+ \ '-': ':call <SID>NM_show_remove_tag()<CR>',
+ \ '<Space>': ':call <SID>NM_show_advance()<CR>',
+ \ '\|': ':call <SID>NM_show_pipe_message()<CR>',
+ \
+ \ '<Enter>': ':call <SID>NM_show_view_attachment()<CR>',
+ \ 'S': ':call <SID>NM_show_save_attachment()<CR>',
+ \
+ \ 'r': ':call <SID>NM_show_reply()<CR>',
+ \ 'R': ':call <SID>NM_show_view_raw_message()<CR>',
+ \ 'm': ':call <SID>NM_new_mail()<CR>',
+ \ }
+
+" --- --- bindings for compose screen {{{2
+let g:nm_vimpy_compose_nmaps = {
+ \ ',s': ':call <SID>NM_compose_send()<CR>',
+ \ ',a': ':call <SID>NM_compose_attach()<CR>',
+ \ ',q': ':call <SID>NM_kill_this_buffer()<CR>',
+ \ '<Tab>': ':call <SID>NM_compose_next_entry_area()<CR>',
+ \ }
+
+let g:nm_vimpy_raw_message_nmaps = {
+ \ 'q': ':call <SID>NM_kill_this_buffer()<CR>',
+ \ }
+
+" --- implement folders screen {{{1
+
+" Create the folders buffer.
+" Takes a list of [ folder name, query string]
+function! s:NM_cmd_folders(folders)
+ call <SID>NM_create_buffer('folders')
+ silent 0put!='<Enter>:view <Space>:view unread s:search =:refresh tt:all tags m:new mail'
+ python nm_vim.SavedSearches(vim.eval("a:folders"))
+ call <SID>NM_finalize_menu_buffer()
+ call <SID>NM_set_map('n', g:nm_vimpy_folders_maps)
+endfunction
+
+" Show a folder for each existing tag.
+function! s:NM_folders_from_tags()
+ let folders = []
+ python nm_vim.vim_get_tags()
+ for tag in split(taglist, '\n')
+ call add(folders, [tag, 'tag:' . tag ])
+ endfor
+
+ call <SID>NM_cmd_folders(folders)
+endfunction
+
+" --- --- folders screen action functions {{{2
+
+" Refresh the folders screen
+function! s:NM_folders_refresh_view()
+ let lno = line('.')
+ setlocal modifiable
+ silent norm 3GdG
+ python nm_vim.get_current_buffer().refresh()
+ setlocal nomodifiable
+ exec printf('norm %dG', lno)
+endfunction
+
+" Show contents of the folder corresponding to current line AND query
+function! s:NM_folders_show_search(query)
+ exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))
+ if exists('obj')
+ if len(a:query)
+ let querystr = '(' . obj['id'] . ') and ' . a:query
+ else
+ let querystr = obj['id']
+ endif
+
+ call <SID>NM_cmd_search(querystr, 0)
+ endif
+endfunction
+
+" Create the search buffer corresponding to querystr.
+" If relative is 1, the search is relative to current buffer
+function! s:NM_cmd_search(querystr, relative)
+ let cur_buf = bufnr('%')
+ call <SID>NM_create_buffer('search')
+ silent 0put!=printf(' Query results: %s', a:querystr)
+ if a:relative
+ python nm_vim.Search(querystr = vim.eval("a:querystr"), parent = nm_vim.nm_buffers[vim.eval('cur_buf')])
+ else
+ python nm_vim.Search(querystr = vim.eval("a:querystr"))
+ endif
+ call <SID>NM_finalize_menu_buffer()
+ call <SID>NM_set_map('n', g:nm_vimpy_search_maps)
+endfunction
+
+" --- --- search screen action functions {{{2
+
+" Show the thread corresponding to current line
+function! s:NM_search_show_thread()
+ let querystr = <SID>NM_search_thread_id()
+ if len(querystr)
+ call <SID>NM_cmd_show(querystr)
+ endif
+endfunction
+
+" Same as NM_search_show_thread, except jump to first unread
+function! s:NM_search_show_thread_unread()
+ call <SID>NM_search_show_thread()
+ python nm_vim.vim_get_message_for_tag('unread')
+ if start > 0
+ exec printf('norm %dGzt', start)
+ silent! norm zo
+ endif
+endfunction
+
+" Search according to input from user.
+" If edit is 1, current query string is inserted to prompt for editing.
+function! s:NM_search_prompt(edit)
+ if a:edit
+ python nm_vim.vim_get_id()
+ else
+ let buf_id = ''
+ endif
+ let querystr = input('Search: ', buf_id, 'custom,NM_search_type_completion')
+ if len(querystr)
+ call <SID>NM_cmd_search(querystr, 0)
+ endif
+endfunction
+
+" Filter current search, i.e. search for
+" (current querystr) AND (user input)
+function! s:NM_search_filter()
+ let querystr = input('Filter: ', '', 'custom,NM_search_type_completion')
+ if len(querystr)
+ call <SID>NM_cmd_search(querystr, 1)
+ endif
+endfunction
+
+""""""""""""""""""""""'' TODO
+function! s:NM_search_archive_thread()
+ call <SID>NM_tag([], ['-inbox'])
+ norm j
+endfunction
+
+function! s:NM_search_mark_read_then_archive_thread()
+ call <SID>NM_tag([], ['-unread', '-inbox'])
+ norm j
+endfunction
+
+function! s:NM_search_delete_thread()
+ call <SID>NM_tag([], ['+junk','-inbox','-unread'])
+ norm j
+endfunction
+
+"""""""""""""""""""""""""""""""""""""""""""""""""""""
+
+" XXX This function is broken
+function! s:NM_search_toggle_order()
+ let g:notmuch_search_newest_first = !g:notmuch_search_newest_first
+ " FIXME: maybe this would be better done w/o reading re-reading the lines
+ " reversing the b:nm_raw_lines and the buffer lines would be better
+ call <SID>NM_search_refresh_view()
+endfunction
+
+"XXX this function is broken
+function! s:NM_search_reply_to_thread()
+ python vim.command('let querystr = "%s"'%nm_vim.get_current_buffer().id)
+ let cmd = ['reply']
+ call add(cmd, <SID>NM_search_thread_id())
+ call add(cmd, 'AND')
+ call extend(cmd, [querystr])
+
+ let data = <SID>NM_run(cmd)
+ let lines = split(data, "\n")
+ call <SID>NM_newComposeBuffer(lines, 0)
+endfunction
+
+function! s:NM_search_add_tags(tags)
+ call <SID>NM_search_add_remove_tags('Add Tag(s): ', '+', a:tags)
+endfunction
+
+function! s:NM_search_remove_tags(tags)
+ call <SID>NM_search_add_remove_tags('Remove Tag(s): ', '-', a:tags)
+endfunction
+
+function! s:NM_search_refresh_view()
+ let lno = line('.')
+ setlocal modifiable
+ norm 3ggdG
+ python nm_vim.get_current_buffer().refresh()
+ setlocal nomodifiable
+ " FIXME: should find the line of the thread we were on if possible
+ exec printf('norm %dG', lno)
+endfunction
+
+" --- --- search screen helper functions {{{2
+
+function! s:NM_search_thread_id()
+ exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))
+ if exists('obj')
+ return 'thread:' . obj['id']
+ endif
+ return ''
+endfunction
+
+function! s:NM_search_add_remove_tags(prompt, prefix, intags)
+ if type(a:intags) != type([]) || len(a:intags) == 0
+ let text = input(a:prompt, '', 'custom,NM_tag_name_completion')
+ if !strlen(text)
+ return
+ endif
+ let tags = split(text, ' ')
+ else
+ let tags = a:intags
+ endif
+ call map(tags, 'a:prefix . v:val')
+ call <SID>NM_tag([], tags)
+endfunction
+
+" --- implement show screen {{{1
+
+function! s:NM_cmd_show(querystr)
+ "TODO: folding, syntax
+ call <SID>NM_create_buffer('show')
+ python nm_vim.ShowThread(vim.eval('a:querystr'))
+
+ call <SID>NM_set_map('n', g:nm_vimpy_show_maps)
+ setlocal fillchars=
+ setlocal foldtext=NM_show_foldtext()
+ setlocal foldcolumn=6
+ setlocal foldmethod=syntax
+ setlocal nomodifiable
+ setlocal nowrap
+ call <SID>NM_jump_message(1)
+endfunction
+
+function! s:NM_jump_message(offset)
+ "TODO implement can_change_thread and find_matching, nicer positioning
+ exec printf('python nm_vim.vim_get_object(%d, %d)', line('.'), a:offset)
+ if exists('obj')
+ silent! norm zc
+ exec printf('norm %dGzt', obj['start'])
+ silent! norm zo
+ endif
+endfunction
+
+function! s:NM_show_next_thread()
+ call <SID>NM_kill_this_buffer()
+ if line('.') != line('$')
+ norm j
+ call <SID>NM_search_show_thread()
+ else
+ echo 'No more messages.'
+ endif
+endfunction
+
+function! s:NM_show_archive_thread()
+ call <SID>NM_tag('', ['-inbox'])
+ call <SID>NM_show_next_thread()
+endfunction
+
+function! s:NM_show_mark_read_then_archive_thread()
+ call <SID>NM_tag('', ['-unread', '-inbox'])
+ call <SID>NM_show_next_thread()
+endfunction
+
+function! s:NM_show_mark_read_then_next_open_message()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_previous_message()
+ echo 'not implemented'
+endfunction
+
+"XXX pythonise
+function! s:NM_show_reply()
+ let cmd = ['reply']
+ call add(cmd, 'id:' . <SID>NM_show_message_id())
+
+ let data = <SID>NM_run(cmd)
+ let lines = split(data, "\n")
+ call <SID>NM_newComposeBuffer(lines, 0)
+endfunction
+
+function! s:NM_show_view_all_mime_parts()
+ echo 'not implemented'
+endfunction
+
+"Show the raw message for current line in a new buffer
+function! s:NM_show_view_raw_message()
+ exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))
+ if !exists('obj')
+ return
+ endif
+
+ call <SID>NM_create_buffer('rawmessage')
+ python nm_vim.RawMessage(vim.eval("obj['id']"))
+ call <SID>NM_set_map('n', g:nm_vimpy_raw_message_nmaps)
+endfunction
+
+function! s:NM_show_add_tag()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_remove_tag()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_advance()
+ let advance_tags = ['-unread']
+
+ exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))
+ if !exists('obj')
+ return
+ endif
+
+ call <SID>NM_tag(['id:' . obj['id']], advance_tags)
+ if obj['end'] == line('$')
+ call <SID>NM_kill_this_buffer()
+ else
+ call <SID>NM_jump_message(1)
+ endif
+endfunction
+
+function! s:NM_show_pipe_message()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_view_attachment()
+ exec printf('python nm_vim.vim_view_attachment(%d)', line('.'))
+endfunction
+
+function! s:NM_show_save_attachment()
+ let filename = input('Where to save the attachment: ', './', 'file')
+ if len(filename)
+ try
+ python nm_vim.vim_save_attachment(int(vim.eval('line(".")')), vim.eval('filename'))
+ echo 'Attachment saved successfully.'
+ endtry
+ endif
+endfunction
+
+" --- --- show screen helper functions {{{2
+
+function! s:NM_show_message_id()
+ exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))
+ if exists('obj')
+ return obj['id']
+ else
+ return ''
+endfunction
+
+" --- implement compose screen {{{1
+
+function! s:NM_cmd_compose(words, body_lines)
+ let lines = []
+ let start_on_line = 0
+
+ let hdrs = { }
+
+ if !has_key(hdrs, 'From') || !len(hdrs['From'])
+ let me = <SID>NM_compose_get_user_email()
+ let hdrs['From'] = [ me ]
+ endif
+
+ for key in g:nm_vimpy_compose_headers
+ let text = has_key(hdrs, key) ? join(hdrs[key], ', ') : ''
+ call add(lines, key . ': ' . text)
+ if !start_on_line && !strlen(text)
+ let start_on_line = len(lines)
+ endif
+ endfor
+
+ for [key,val] in items(hdrs)
+ if match(g:nm_vimpy_compose_headers, key) == -1
+ let line = key . ': ' . join(val, ', ')
+ call add(lines, line)
+ endif
+ endfor
+
+ call add(lines, '')
+ if !start_on_line
+ let start_on_line = len(lines) + 1
+ endif
+
+ call extend(lines, [ '', '' ])
+
+ call <SID>NM_newComposeBuffer(lines, start_on_line)
+endfunction
+
+function! s:NM_compose_send()
+ let fname = expand('%')
+
+ try
+ python nm_vim.get_current_buffer().send()
+ call <SID>NM_kill_this_buffer()
+
+ call delete(fname)
+ echo 'Mail sent successfully.'
+ endtry
+endfunction
+
+function! s:NM_compose_attach()
+ let attachment = input('Enter attachment filename: ', '', 'file')
+ if len(attachment)
+ python nm_vim.get_current_buffer().attach(vim.eval('attachment'))
+ endif
+endfunction
+
+function! s:NM_compose_next_entry_area()
+ let lnum = line('.')
+ let hdr_end = <SID>NM_compose_find_line_match(1,'^$',1)
+ if lnum < hdr_end
+ let lnum = lnum + 1
+ let line = getline(lnum)
+ if match(line, '^\([^:]\+\):\s*$') == -1
+ call cursor(lnum, strlen(line) + 1)
+ return ''
+ endif
+ while match(getline(lnum+1), '^\s') != -1
+ let lnum = lnum + 1
+ endwhile
+ call cursor(lnum, strlen(getline(lnum)) + 1)
+ return ''
+
+ elseif lnum == hdr_end
+ call cursor(lnum+1, strlen(getline(lnum+1)) + 1)
+ return ''
+ endif
+ if mode() == 'i'
+ if !getbufvar(bufnr('.'), '&et')
+ return "\t"
+ endif
+ let space = ''
+ let shiftwidth = a:shiftwidth
+ let shiftwidth = shiftwidth - ((virtcol('.')-1) % shiftwidth)
+ " we assume no one has shiftwidth set to more than 40 :)
+ return ' '[0:shiftwidth]
+ endif
+endfunction
+
+" --- --- compose screen helper functions {{{2
+
+function! s:NM_compose_get_user_email()
+ " TODO: do this properly (still), i.e., allow for multiple email accounts
+ let email = substitute(system('notmuch config get user.primary_email'), '\v(^\s*|\s*$|\n)', '', 'g')
+ return email
+endfunction
+
+function! s:NM_compose_find_line_match(start, pattern, failure)
+ let lnum = a:start
+ let lend = line('$')
+ while lnum < lend
+ if match(getline(lnum), a:pattern) != -1
+ return lnum
+ endif
+ let lnum = lnum + 1
+ endwhile
+ return a:failure
+endfunction
+
+
+" --- notmuch helper functions {{{1
+function! s:NM_create_buffer(type)
+ let prev_bufnr = bufnr('%')
+
+ enew
+ setlocal buftype=nofile
+ setlocal bufhidden=hide
+ execute printf('set filetype=nm_vimpy-%s', a:type)
+ execute printf('set syntax=nm_vimpy-%s', a:type)
+ "XXX this should probably go
+ let b:nm_prev_bufnr = prev_bufnr
+endfunction
+
+"set some options for "menu"-like buffers -- folders/searches
+function! s:NM_finalize_menu_buffer()
+ setlocal nomodifiable
+ setlocal cursorline
+ setlocal nowrap
+endfunction
+
+function! s:NM_newBuffer(how, type, content)
+ if strlen(a:how)
+ exec a:how
+ else
+ enew
+ endif
+ setlocal buftype=nofile readonly modifiable scrolloff=0 sidescrolloff=0
+ silent put=a:content
+ keepjumps 0d
+ setlocal nomodifiable
+ execute printf('set filetype=notmuch-%s', a:type)
+ execute printf('set syntax=notmuch-%s', a:type)
+endfunction
+
+function! s:NM_newFileBuffer(fdir, fname, type, lines)
+ let fdir = expand(a:fdir)
+ if !isdirectory(fdir)
+ call mkdir(fdir, 'p')
+ endif
+ let file_name = <SID>NM_mktemp(fdir, a:fname)
+ if writefile(a:lines, file_name)
+ throw 'Eeek! couldn''t write to temporary file ' . file_name
+ endif
+ exec printf('edit %s', file_name)
+ setlocal buftype= noreadonly modifiable scrolloff=0 sidescrolloff=0
+ execute printf('set filetype=notmuch-%s', a:type)
+ execute printf('set syntax=notmuch-%s', a:type)
+endfunction
+
+function! s:NM_newComposeBuffer(lines, start_on_line)
+ let lines = a:lines
+ let start_on_line = a:start_on_line
+ let real_hdr_start = 1
+ if g:notmuch_compose_header_help
+ let help_lines = [
+ \ 'Notmuch-Help: Type in your message here; to help you use these bindings:',
+ \ 'Notmuch-Help: ,a - attach a file',
+ \ 'Notmuch-Help: ,s - send the message (Notmuch-Help lines will be removed)',
+ \ 'Notmuch-Help: ,q - abort the message',
+ \ 'Notmuch-Help: <Tab> - skip through header lines',
+ \ ]
+ call extend(lines, help_lines, 0)
+ let real_hdr_start = len(help_lines)
+ if start_on_line > 0
+ let start_on_line = start_on_line + len(help_lines)
+ endif
+ endif
+ if exists('g:nm_vimpy_signature')
+ call extend(lines, ['', '-- '])
+ call extend(lines, g:nm_vimpy_signature)
+ endif
+
+
+ let prev_bufnr = bufnr('%')
+ call <SID>NM_newFileBuffer(g:notmuch_compose_temp_file_dir, '%s.mail',
+ \ 'compose', lines)
+ let b:nm_prev_bufnr = prev_bufnr
+
+ call <SID>NM_set_map('n', g:nm_vimpy_compose_nmaps)
+
+ if start_on_line > 0 && start_on_line <= len(lines)
+ call cursor(start_on_line, strlen(getline(start_on_line)) + 1)
+ else
+ call cursor(real_hdr_start, strlen(getline(real_hdr_start)) + 1)
+ call <SID>NM_compose_next_entry_area()
+ endif
+
+ if g:notmuch_compose_insert_mode_start
+ startinsert!
+ endif
+
+ python nm_vim.Compose()
+endfunction
+
+function! s:NM_mktemp(dir, name)
+ let time_stamp = strftime('%Y%m%d-%H%M%S')
+ let file_name = substitute(a:dir,'/*$','/','') . printf(a:name, time_stamp)
+ " TODO: check if it exists, try again
+ return file_name
+endfunction
+
+function! s:NM_shell_escape(word)
+ " TODO: use shellescape()
+ let word = substitute(a:word, '''', '\\''', 'g')
+ return '''' . word . ''''
+endfunction
+
+function! s:NM_run(args)
+ let words = a:args
+ call map(words, 's:NM_shell_escape(v:val)')
+ let cmd = g:notmuch_cmd . ' ' . join(words) . '< /dev/null'
+
+ let out = system(cmd)
+ let err = v:shell_error
+
+ if err
+ echohl Error
+ echo substitute(out, '\n*$', '', '')
+ echohl None
+ return ''
+ else
+ return out
+ endif
+endfunction
+
+" --- external mail handling helpers {{{1
+
+function! s:NM_new_mail()
+ call <SID>NM_cmd_compose([], [])
+endfunction
+
+" --- tag manipulation helpers {{{1
+
+" used to combine an array of words with prefixes and separators
+" example:
+" NM_combine_tags('tag:', ['one', 'two', 'three'], 'OR', '()')
+" -> ['(', 'tag:one', 'OR', 'tag:two', 'OR', 'tag:three', ')']
+function! s:NM_combine_tags(word_prefix, words, separator, brackets)
+ let res = []
+ for word in a:words
+ if len(res) && strlen(a:separator)
+ call add(res, a:separator)
+ endif
+ call add(res, a:word_prefix . word)
+ endfor
+ if len(res) > 1 && strlen(a:brackets)
+ if strlen(a:brackets) != 2
+ throw 'Eeek! brackets arg to NM_combine_tags must be 2 chars'
+ endif
+ call insert(res, a:brackets[0])
+ call add(res, a:brackets[1])
+ endif
+ return res
+endfunction
+
+" --- other helpers {{{1
+
+function! s:NM_kill_this_buffer()
+ let prev_bufnr = b:nm_prev_bufnr
+ python nm_vim.delete_current_buffer()
+ bdelete!
+ exec printf("buffer %d", prev_bufnr)
+endfunction
+
+function! s:NM_search_expand(arg)
+ let word = expand(a:arg)
+ let prev_bufnr = bufnr('%')
+ call <SID>NM_cmd_search(word, 0)
+ let b:nm_prev_bufnr = prev_bufnr
+endfunction
+
+function! s:NM_tag(filter, tags)
+ let filter = len(a:filter) ? a:filter : [<SID>NM_search_thread_id()]
+ if !len(filter)
+ throw 'Eeek! I couldn''t find the thead id!'
+ endif
+ python nm_vim.get_current_buffer().tag(tags = vim.eval("a:tags"), querystr = vim.eval('join(filter)'))
+endfunction
+
+" --- process and set the defaults {{{1
+
+function! s:NM_set_defaults(force)
+ setlocal bufhidden=hide
+ for [key, dflt] in items(s:nm_vimpy_defaults)
+ let cmd = ''
+ if !a:force && exists(key) && type(dflt) == type(eval(key))
+ continue
+ elseif type(dflt) == type(0)
+ let cmd = printf('let %s = %d', key, dflt)
+ elseif type(dflt) == type('')
+ let cmd = printf('let %s = ''%s''', key, dflt)
+ " FIXME: not sure why this didn't work when dflt is an array
+ "elseif type(dflt) == type([])
+ " let cmd = printf('let %s = %s', key, string(dflt))
+ else
+ echoe printf('E: Unknown type in NM_set_defaults(%d) using [%s,%s]',
+ \ a:force, key, string(dflt))
+ continue
+ endif
+ exec cmd
+ endfor
+endfunction
+call <SID>NM_set_defaults(0)
+
+" for some reason NM_set_defaults() didn't work for arrays...
+if !exists('g:nm_vimpy_folders')
+ let g:nm_vimpy_folders = s:nm_vimpy_folders_defaults
+endif
+
+if !exists('g:nm_vimpy_show_headers')
+ let g:nm_vimpy_show_headers = s:nm_vimpy_show_headers_defaults
+endif
+
+if !exists('g:nm_vimpy_signature')
+ if filereadable(glob('~/.signature'))
+ let g:nm_vimpy_signature = readfile(glob('~/.signature'))
+ endif
+endif
+if !exists('g:nm_vimpy_compose_headers')
+ let g:nm_vimpy_compose_headers = s:nm_vimpy_compose_headers_defaults
+endif
+
+" --- assign keymaps {{{1
+
+function! s:NM_set_map(type, maps)
+ for [key, code] in items(a:maps)
+ exec printf('%snoremap <buffer> %s %s', a:type, key, code)
+ endfor
+endfunction
+
+" --- command handler {{{1
+
+function! NMVimpy()
+ call <SID>NM_cmd_folders(g:nm_vimpy_folders)
+endfunction
+
+"Custom foldtext() for show buffers, which indents folds to
+"represent thread structure
+function! NM_show_foldtext()
+ if v:foldlevel != 1
+ return foldtext()
+ endif
+ let indentlevel = matchstr(getline(v:foldstart), '^[0-9]\+')
+ return repeat(' ', indentlevel) . getline(v:foldstart + 1)
+endfunction
+
+"Completion of search prompt
+function! NM_search_type_completion(arg_lead, cmd_line, cursor_pos)
+ let keywords = 'from:' . "\n" .
+ \ 'to:' . "\n" .
+ \ 'subject:' . "\n" .
+ \ 'attachment:' . "\n" .
+ \ 'tag:' . "\n" .
+ \ 'id:' . "\n" .
+ \ 'thread:' . "\n" .
+ \ 'folder:' . "\n" .
+ \ 'and' . "\n" .
+ \ 'or'
+ let s_idx = strridx(a:arg_lead, " ")
+ let col_idx = stridx(a:arg_lead, ":", s_idx)
+ let prefix = strpart(a:arg_lead, 0, s_idx + 1)
+ if col_idx < 0
+ return prefix . substitute(keywords, "\n", "\n" . prefix, "g")
+ endif
+ if stridx(a:arg_lead, 'tag:', s_idx) >= 0
+ python nm_vim.vim_get_tags()
+ return prefix . 'tag:' . substitute(taglist, "\n", "\n" . prefix . 'tag:', "g")
+ endif
+ return ''
+endfunction
+
+function! NM_tag_name_completion(arg_lead, cmd_line, cursor_pos)
+ let s_idx = strridx(a:arg_lead, " ")
+ let prefix = strpart(a:arg_lead, 0, s_idx + 1)
+ python nm_vim.vim_get_tags()
+ return prefix . substitute(taglist, "\n", "\n" . prefix, "g")
+endfunction
+
+let s:notmuch_loaded = 1
diff --git a/vim/syntax/nm_vimpy-compose.vim b/vim/syntax/nm_vimpy-compose.vim
new file mode 100644
index 0000000..19adb75
--- /dev/null
+++ b/vim/syntax/nm_vimpy-compose.vim
@@ -0,0 +1,7 @@
+runtime! syntax/mail.vim
+
+syntax region nmComposeHelp contains=nmComposeHelpLine start='^Notmuch-Help:\%1l' end='^\(Notmuch-Help:\)\@!'
+syntax match nmComposeHelpLine /Notmuch-Help:/ contained
+
+highlight link nmComposeHelp Include
+highlight link nmComposeHelpLine Error
diff --git a/vim/syntax/nm_vimpy-folders.vim b/vim/syntax/nm_vimpy-folders.vim
new file mode 100644
index 0000000..fab6956
--- /dev/null
+++ b/vim/syntax/nm_vimpy-folders.vim
@@ -0,0 +1,15 @@
+" notmuch folders mode syntax file
+
+syntax region nmFolfers start=/^/ end=/$/ oneline contains=nmFoldersMessageCount
+syntax match nmFoldersMessageCount /^ *[0-9]\+ */ contained nextgroup=nmFoldersUnreadCount
+syntax match nmFoldersUnreadCount /(.\{-}) */ contained nextgroup=nmFoldersName
+syntax match nmFoldersName /.*\ze(/ contained nextgroup=nmFoldersSearch
+syntax match nmFoldersSearch /([^()]\+)$/
+
+highlight link nmFoldersMessageCount Statement
+highlight link nmFoldersUnreadCount Underlined
+highlight link nmFoldersName Type
+highlight link nmFoldersSearch String
+
+highlight CursorLine term=reverse cterm=reverse gui=reverse
+
diff --git a/vim/syntax/nm_vimpy-search.vim b/vim/syntax/nm_vimpy-search.vim
new file mode 100644
index 0000000..4a19329
--- /dev/null
+++ b/vim/syntax/nm_vimpy-search.vim
@@ -0,0 +1,18 @@
+syntax region nmSearch start=/^/ end=/$/ oneline contains=nmSearchDate keepend
+syntax match nmSearchDate /^.\{-13}/ contained nextgroup=nmSearchNum skipwhite
+syntax match nmSearchNum "[0-9]\+\/" contained nextgroup=nmSearchTotal skipwhite
+syntax match nmSearchTotal /[0-9]\+/ contained nextgroup=nmSearchFrom skipwhite
+syntax match nmSearchFrom /.\{-}\ze|/ contained nextgroup=nmSearchSubject skipwhite
+"XXX this fails on some messages with multiple authors
+syntax match nmSearchSubject /.*\ze(/ contained nextgroup=nmSearchTags,nmUnread
+syntax match nmSearchTags /.\+$/ contained
+syntax match nmUnread /.*\<unread\>.*)$/ contained
+
+highlight link nmSearchDate Statement
+highlight link nmSearchNum Number
+highlight link nmSearchTotal Type
+highlight link nmSearchFrom Include
+highlight link nmSearchSubject Normal
+highlight link nmSearchTags String
+
+highlight link nmUnread Underlined
diff --git a/vim/syntax/nm_vimpy-show.vim b/vim/syntax/nm_vimpy-show.vim
new file mode 100644
index 0000000..d21eb45
--- /dev/null
+++ b/vim/syntax/nm_vimpy-show.vim
@@ -0,0 +1,23 @@
+" notmuch show mode syntax file
+
+setlocal conceallevel=2
+setlocal concealcursor=vinc
+
+syntax region nmMessage matchgroup=Ignore concealends start='[0-9]\+\/-*message start-*\\' end='\\-*message end-*\/' fold contains=@nmShowMsgBody keepend
+
+"TODO what about those
+syntax cluster nmShowMsgDesc contains=nmShowMsgDescWho,nmShowMsgDescDate,nmShowMsgDescTags
+syntax match nmShowMsgDescWho /[^)]\+)/ contained
+syntax match nmShowMsgDescDate / ([^)]\+[0-9]) / contained
+syntax match nmShowMsgDescTags /([^)]\+)$/ contained
+
+syntax cluster nmShowMsgBody contains=@nmShowMsgBodyMail,@nmShowMsgBodyGit
+syntax include @nmShowMsgBodyMail syntax/mail.vim
+silent! syntax include @nmShowMsgBodyGit syntax/notmuch-git-diff.vim
+
+highlight nmShowMsgDescWho term=reverse cterm=reverse gui=reverse
+highlight link nmShowMsgDescDate Type
+highlight link nmShowMsgDescTags String
+
+"TODO what about this?
+highlight Folded term=reverse ctermfg=LightGrey ctermbg=Black guifg=LightGray guibg=Black