docwhat's avatardocwhat's blog

GTD and Mutt

I found this article about Getting things done with mutt. It talks about using the techniques from David Allen’s productivity book ”Getting Things Done: The Art of Stress-Free Productivity. I’ve been reading GTD for a week now (taking my time, you know) and think it’s pretty interesting.

Since Mutt is my favorite email client, I spent a little playing with it and making changes. I’m using a different editlabel script and my way doesn’t require patching and recompiling mutt.

Here are my muttrc changes:

set editor='emacs'
set move=yes
set mbox='=archive'

unignore X-Label:  # make sure to display X-Label on each message
color header red default '^X-Label:'
mailboxes =ACTION
mailboxes =RESPOND
mailboxes =WAITFOR
mbox-hook =ACTION  =archive
mbox-hook =RESPOND =archive
macro pager \Ct  "<exit><tag -entry></tag><tag -prefix><mark -as-new><tag -prefix><save -message>=ACTION<enter>"  "Mark message as ACTION"
macro pager \Cr  "<exit><tag -entry></tag><tag -prefix><mark -as-new><tag -prefix><save -message>=RESPOND<enter>" "Mark message as RESPOND"
macro pager \Cw  "<exit><tag -entry></tag><tag -prefix><mark -as-new><tag -prefix><save -message>=WAITFOR<enter>" "Mark message as WAITFOR"
macro index y "</enter><enter -command>set editor=\"~/comp/editlabel\"\n\
<edit><enter -command>set editor=\"emacs -nw\"\n\
<sync -mailbox><next -undeleted>" "Edit Label"
macro pager y "<enter -command>set editor=\"~/.mutt/editlabel\"\n\
<edit><sync -mailbox><next -undeleted>\
<enter -command>set editor=emacs\n" "Edit Label"
macro index \Cy "<limit>~y " "Limit view to label"

set index_format="%4C %Z %{%b %d} %-15.15L %?M?(#%03M)&(%4l)? %?y?{%.20Y} ?%s"
#### END GTD ###

editlabel also has full readline editing and history, which is nice. I haven’t added tab-completion from the history; I’m not sure if I will or not.

My editlabel also has the advantage that you can delete a label just by deleting the line; ^a ^k if you use emacs.

Note that if you use the above, you’ll have to replace the occurrences of “emacs” with whatever editor you use.

I just found a similar post: GTD with Mutt

I like my editlabel better, even if I’m using Python’s evil os.system(). But that narrow-wide trick is neat.


  1. UPDATE 2007-10-08: Added mark-as-new when saving, so that they will attract my notice via the mailbox changing.
  2. UPDATE 2007-10-09: Fixed the mark-as-new stuff…had to use tagging to make it work.
  3. UPDATE 2010-03-01: Updated ‘y’ macro based on feedback from Mark Fardal.
  4. UPDATE 2014-08-11: Fixed broken formatting due to age. Fixed some minor typos.


Gravatar for sr

Thank you! Very useful, even outside of GTD: just for the labels.

>I like my editlabel better The same. But cannot point to anything specific (maybe readline).

>But that narrow-wide trick is neat. What is it?

>evil os.system() Maybe a comment about the dependancy on formail command could help?

>I haven’t added tab-completion from the >history; I’m not sure if I will or not. If this is not too much work, it could be appreciated: for emacs users, tab completion feels natural.

Minor comment: instead of duplicating the command in the muttrc, one can use: macro index,pager

Gravatar for klaus weidner
Klaus Weidner


thanks for the writeup, this is very close to what I was looking for.

I’ve been hacking on it a bit to add label completion with the Tab key, and a prompt when using new labels to check if you want to add them to the label list (to avoid typos).

I’m keeping everything in my Inbox (including my own sent messages) and adding labels like “Done” to messages I don’t want to see in the default view (!~y Done), but they are still available for other searches.

Also, I’ve used a dirty hack to make the label function work for multiple tagged messages by saving the actions to do (add/remove/set labels) to a temporary file.

Replace “Mutt-edit” with vim/emacs/editor of your choice, and note that I’ve changed the paths.

* .muttrc

[code] macro index,pager y ”<enter-command>set editor=&quot;editlabel&quot;\n
<shell-escape>rm -f ~/.labelsavedaction<enter>
<shell-escape>rm -f ~/.labelsavedaction<enter>
<enter-command>set editor=Mutt-edit\n” ”Edit Label” [/code]

**** editlabel

[code lang=“python”]

!/usr/bin/python -utWall

import readline, sys, os

histfile = os.path.join(os.environ[”HOME”], ”.labelhistory”) labelfile = os.path.join(os.environ[”HOME”], ”.labels”) actfile = os.path.join(os.environ[”HOME”], ”.labelsaved_action”)

def getlabelfile(): try: return map(lambda s: s.rstrip(), open(labelfile, ‘r’).readlines()) except IOError: return

def labelcompletions(text, state): labels = getlabel_file() candidates = filter(lambda x: x.startswith(text), labels) try: return candidates[state] except: return

def myinput(prompt, default=None, completions=None): if default is not None: def preinputhook(): readline.inserttext(str(default)) readline.redisplay() readline.setpreinputhook(preinput_hook) val = []

readline.parseandbind(”tab: complete”) readline.setcompleter(completions) return rawinput(prompt)

def get_label(filename): header = ‘X-Label: ’ fp = file(filename, ‘r’) result = ” for line in fp.readlines(): if line.startswith(header): result = line[len(header):].strip() break fp.close() return result

def write_label(filename, label): header = [‘X-Label:’] tmpfile = filename + ‘.tmp’ if label: header.append(’ ’) header.append(label.rstrip().lstrip()) header = ’”%s”’ % ((”.join(header)).replace(’”’, ’&quot;’)) cmd = ’ ‘.join([ ‘formail’, ‘-I’, header, ’<’, filename, ’>’, tmpfile, ’&&’, ‘mv’, tmpfile, filename ]) os.system(cmd)

def ask(prompt, default): print prompt + ’ [‘+default+’] ’, ans = sys.stdin.readline().rstrip() if ans == ””: return default return ans

def editlabel(label): while True: label = myinput(‘Label: ’, label, labelcompletions) knownlabels = set(getlabelfile()) unknown = filter(lambda x: not x in knownlabels, label.split()) if len(unknown) == 0: break else: notadded=0 for new in unknown: if askyn(”label ’”+new+”’ not known, add?”, ‘n’): open(labelfile, ”a”).write(new+‘\n’) else: notadded += 1 if not_added==0: break

return label

def doaction(action, infile, newlabel=None): if action == ‘a’ or action == ’+’: if newlabel == None: newlabel = editlabel(””) currentlabels = getlabel(infile) updatedlabels = currentlabels for new in newlabel.split(): if not new in set(currentlabels.split()): updatedlabels += ’ ’ + new if updatedlabels != currentlabels: writelabel(infile, updatedlabels) return new_label

elif action == ‘r’ or action == ’-’: rmlabel = newlabel if rmlabel == None: rmlabel = editlabel(””) currentlabels = getlabel(infile) updatedlabels = ”” for new in currentlabels.split(): if not new in set(rmlabel.split()): updatedlabels += ’ ’ + new if updatedlabels != currentlabels: writelabel(infile, updatedlabels) return rmlabel

elif action == ‘e’ or action == ’=’: label = getlabel(infile) if newlabel == None: newlabel = editlabel(label) if newlabel != label: writelabel(infile, newlabel) return newlabel

def ask_yn(prompt, default): return ask(prompt, default) == ‘y’

if ”main” == name: infile = sys.argv[1]

if hasattr(readline, ‘readhistoryfile’): try: readline.readhistoryfile(histfile) except IOError: pass

try: actions = open(actfile, ‘r’).readlines() action = actions[0].rstrip() newlabel = actions[1].rstrip() doaction(action, infile, newlabel) except IOError: # ask user os.system(”clear”) action = ask(”(a)dd, (r)emove, or (e)dit labels?”, ”a”) newlabel = do_action(action, infile)

   open(actfile, 'w').write(action + '\n' + new_label + '\n')


Gravatar for marc

Your version of editlabel is exactly what I’m looking for (in particular, applying it to tagged message).

Unfortunately, I get the following error when I run it:

Traceback (most recent call last): File “editlabel”, line 148, in newlabel = doaction(action, infile) File “editlabel”, line 122, in doaction newlabel = editlabel(label) File “editlabel”, line 77, in editlabel knownlabels = set(getlabel_file()) TypeError: ‘NoneType’ object is not iterable

I get this when running editlabel , where is a mail file. Can you assist me?


Gravatar for docwhat

“editlabel” is only 64 lines long; I suspect that you cut-and-pasted it and your editor wrapped lines. Try downloading the file directly instead of cut-and-paste.

Submit a Comment


The personal blog of Christian Höltje.
docwhat docwhat contact