From c71aca4f5479600a9a9c20ccc12cf8fc2b67f694 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Wed, 17 Aug 2011 23:03:43 +0100 Subject: addressbook class and abook parser --- alot/account.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/alot/account.py b/alot/account.py index 720d3878..bcb69db0 100644 --- a/alot/account.py +++ b/alot/account.py @@ -23,6 +23,8 @@ import subprocess import logging import time import email +import os +from ConfigParser import SafeConfigParser from urlparse import urlparse @@ -220,3 +222,32 @@ class AccountManager: def get_addresses(self): """returns addresses of known accounts including all their aliases""" return self.accountmap.keys() + + +class AddressBook: + def get_contacts(self): + return [] + + def lookup(self, prefix=''): + res = [] + for name, email in self.get_contacts(): + if name.startswith(prefix) or email.startswith(prefix): + res.append("%s <%s>" % (name, email)) + return res + + +class AbookAddressBook(AddressBook): + def __init__(self, config=None): + self.abook = SafeConfigParser() + if not config: + config = os.environ["HOME"] + "/.abook/addressbook" + self.abook.read(config) + + def get_contacts(self): + res = [] + for s in self.abook.sections(): + if s.isdigit(): + name = self.abook.get(s, 'name') + email = self.abook.get(s, 'email') + res.append((name,email)) + return res -- cgit v1.2.3 From 53107603db05d878627154eaece5c0ce88830fc1 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sat, 20 Aug 2011 15:51:44 +0100 Subject: shlex and unicode (needs ascii) --- alot/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alot/helper.py b/alot/helper.py index b1668d80..fbf6643b 100644 --- a/alot/helper.py +++ b/alot/helper.py @@ -44,7 +44,7 @@ def pretty_datetime(d): def cmd_output(command_line): - args = shlex.split(command_line) + args = shlex.split(command_line.encode('ascii', errors='ignore')) try: output = subprocess.check_output(args) except subprocess.CalledProcessError: -- cgit v1.2.3 From a765b56bec47c5f1075efea3c4591ec48d35024b Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sat, 20 Aug 2011 15:55:42 +0100 Subject: AddressBook classes that can lookup names/emails --- alot/account.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/alot/account.py b/alot/account.py index bcb69db0..2e1fae81 100644 --- a/alot/account.py +++ b/alot/account.py @@ -22,11 +22,14 @@ import shlex import subprocess import logging import time +import re import email import os from ConfigParser import SafeConfigParser from urlparse import urlparse +from helper import cmd_output + class Account: """ @@ -44,10 +47,13 @@ class Account: """gpg fingerprint. CURRENTLY IGNORED""" signature = None """signature to append to outgoing mails. CURRENTLY IGNORED""" + abook = None def __init__(self, address=None, aliases=None, realname=None, gpg_key=None, - signature=None, sent_box=None, draft_box=None): + signature=None, sent_box=None, draft_box=None, + abook=None): self.address = address + self.abook = abook self.aliases = [] if aliases: self.aliases = aliases.split(';') @@ -164,6 +170,8 @@ class AccountManager: 'signature', 'type', 'sendmail_command', + 'abook_command', + 'abook_regexp', 'sent_box', 'draft_box'] manditory = ['realname', 'address'] @@ -176,7 +184,20 @@ class AccountManager: accountsections = filter(lambda s: s.startswith('account '), sections) for s in accountsections: options = filter(lambda x: x in self.allowed, config.options(s)) + + args = {} + if 'abook_command' in options: + cmd = config.get(s, 'abook_command').encode('ascii', + errors='ignore') + options.remove('abook_command') + if 'abook_regexp' in options: + rgexp = config.get(s, 'abook_regexp') + options.remove('abook_regexp') + else: + regexp = None + args['abook'] = MatchSdtoutAddressbook(cmd, match=regexp) + to_set = self.manditory for o in options: args[o] = config.get(s, o) @@ -223,6 +244,9 @@ class AccountManager: """returns addresses of known accounts including all their aliases""" return self.accountmap.keys() + def get_addressbooks(self): + return [a.abook for a in self.accounts if a.abook] + class AddressBook: def get_contacts(self): @@ -249,5 +273,31 @@ class AbookAddressBook(AddressBook): if s.isdigit(): name = self.abook.get(s, 'name') email = self.abook.get(s, 'email') - res.append((name,email)) + res.append((name, email)) + return res + + +class MatchSdtoutAddressbook(AddressBook): + + def __init__(self, command, match=None): + self.command = command + if not match: + self.match = "(?P.+?@.+?)\s+(?P.+)" + else: + self.match = match + + def get_contacts(self): + return self.lookup('\'\'') + + def lookup(self, prefix): + lines = cmd_output('%s %s' % (self.command, prefix)) + lines = lines.replace('\t', ' ' * 4).splitlines() + res = [] + for l in lines: + m = re.match(self.match, l) + if m: + info = m.groupdict() + email = info['email'].strip() + name = info['name'].strip() + res.append((name, email)) return res -- cgit v1.2.3 From 7e17485e77441d0d9c27f77ff7d06c91c6e80a67 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sat, 20 Aug 2011 15:57:41 +0100 Subject: first attempt to contactscompleter --- alot/command.py | 3 ++- alot/completion.py | 26 ++++++++++++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/alot/command.py b/alot/command.py index 3f7578a6..28667770 100644 --- a/alot/command.py +++ b/alot/command.py @@ -380,7 +380,8 @@ class ComposeCommand(Command): #get To header if 'To' not in self.mail: - to = ui.prompt(prefix='To>', completer=ContactsCompleter()) + to = ui.prompt(prefix='To>', + completer=ContactsCompleter(ui.accountman)) self.mail['To'] = encode_header('to', to) if settings.config.getboolean('general', 'ask_subject') and \ not 'Subject' in self.mail: diff --git a/alot/completion.py b/alot/completion.py index 716554c3..e3821650 100644 --- a/alot/completion.py +++ b/alot/completion.py @@ -19,6 +19,7 @@ Copyright (C) 2011 Patrick Totzke import re import os +import logging import command @@ -33,9 +34,10 @@ class Completer: class QueryCompleter(Completer): """completion for a notmuch query string""" # TODO: boolean connectors and braces? - def __init__(self, dbman): + def __init__(self, dbman, accountman): self.dbman = dbman - self._contactscompleter = ContactsCompleter() + self._contactscompleter = ContactsCompleter(accountman, + addressesonly=True) self._tagscompleter = TagsCompleter(dbman) self.keywords = ['tag', 'from', 'to', 'subject', 'attachment', 'is', 'id', 'thread', 'folder'] @@ -77,10 +79,22 @@ class TagsCompleter(Completer): class ContactsCompleter(Completer): """completes contacts""" + def __init__(self, accountman, addressesonly=True): + self.abooks = accountman.get_addressbooks() + self.addressesonly = addressesonly def complete(self, prefix): - # TODO - return [] + if not self.abooks: + return [] + res = [] + for abook in self.abooks: + res = res + abook.lookup(prefix) + logging.debug(res) + if self.addressesonly: + returnlist = [e[len(prefix):] for n,e in res] + else: + returnlist = ["%s <%s>" % (e,n) for n,e in res] + return returnlist class AccountCompleter(Completer): @@ -117,9 +131,9 @@ class CommandLineCompleter(Completer): self.accountman = accountman self.mode = mode self._commandcompleter = CommandCompleter(dbman, mode) - self._querycompleter = QueryCompleter(dbman) + self._querycompleter = QueryCompleter(dbman, accountman) self._tagscompleter = TagsCompleter(dbman) - self._contactscompleter = ContactsCompleter() + self._contactscompleter = ContactsCompleter(accountman) self._pathcompleter = PathCompleter() def complete(self, prefix): -- cgit v1.2.3 From 4ea9ef9dab9202149e851ef1bfff2b3cb29a34d2 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sat, 27 Aug 2011 21:52:50 +0100 Subject: hotfix issue #38 --- alot/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alot/message.py b/alot/message.py index f4cc2f39..f3128eba 100644 --- a/alot/message.py +++ b/alot/message.py @@ -184,7 +184,7 @@ def extract_body(mail): raw_payload = part.get_payload(decode=True) if part.get_content_maintype() == 'text': if enc: - raw_payload = unicode(raw_payload, enc) + raw_payload = unicode(raw_payload, enc, errors='replace') else: raw_payload = unicode(raw_payload, errors='replace') if ctype == 'text/plain': -- cgit v1.2.3 From a227b444c92610caebb05861da69e2222b39c6fc Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sat, 27 Aug 2011 22:20:05 +0100 Subject: change completion semantics --- alot/completion.py | 16 ++++++++-------- alot/widgets.py | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/alot/completion.py b/alot/completion.py index e3821650..27e81bdc 100644 --- a/alot/completion.py +++ b/alot/completion.py @@ -54,7 +54,7 @@ class QueryCompleter(Completer): else: plen = len(prefix) matched = filter(lambda t: t.startswith(prefix), self.keywords) - return [t[plen:] + ':' for t in matched] + return [t + ':' for t in matched] class TagsCompleter(Completer): @@ -70,7 +70,7 @@ class TagsCompleter(Completer): if len(otags) > 1 and last: return [] else: - matching = [t[len(prefix):] for t in tags if t.startswith(prefix)] + matching = [t for t in tags if t.startswith(prefix)] if last: return matching else: @@ -79,7 +79,7 @@ class TagsCompleter(Completer): class ContactsCompleter(Completer): """completes contacts""" - def __init__(self, accountman, addressesonly=True): + def __init__(self, accountman, addressesonly=False): self.abooks = accountman.get_addressbooks() self.addressesonly = addressesonly @@ -91,9 +91,9 @@ class ContactsCompleter(Completer): res = res + abook.lookup(prefix) logging.debug(res) if self.addressesonly: - returnlist = [e[len(prefix):] for n,e in res] + returnlist = [e for n,e in res] else: - returnlist = ["%s <%s>" % (e,n) for n,e in res] + returnlist = ["%s <%s>" % x for x in res] return returnlist @@ -105,7 +105,7 @@ class AccountCompleter(Completer): def complete(self, prefix): valids = self.accountman.get_main_addresses() - return [a[len(prefix):] for a in valids if a.startswith(prefix)] + return [a for a in valids if a.startswith(prefix)] class CommandCompleter(Completer): @@ -120,7 +120,7 @@ class CommandCompleter(Completer): cmdlist = command.COMMANDS['global'] cmdlist.update(command.COMMANDS[self.mode]) olen = len(original) - return [t[olen:] + '' for t in cmdlist if t.startswith(original)] + return [t + '' for t in cmdlist if t.startswith(original)] class CommandLineCompleter(Completer): @@ -167,5 +167,5 @@ class PathCompleter(Completer): if os.path.isdir(dir): for f in os.listdir(dir): if f.startswith(fileprefix): - res.append(f[len(fileprefix):]) + res.append(f) return res diff --git a/alot/widgets.py b/alot/widgets.py index 2b811146..02cf6acb 100644 --- a/alot/widgets.py +++ b/alot/widgets.py @@ -190,10 +190,10 @@ class CompleteEdit(urwid.Edit): else: self.focus_in_clist -= 1 if len(self.completion_results) > 1: - suffix = self.completion_results[self.focus_in_clist % + completed = self.completion_results[self.focus_in_clist % len(self.completion_results)] - self.set_edit_text(original + suffix) - self.edit_pos += len(suffix) + self.set_edit_text(completed) + self.edit_pos += len(completed) else: self.set_edit_text(original + ' ') self.edit_pos += 1 -- cgit v1.2.3 From b76b71a8e8110494681b84aea758f018b1dc08c3 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 28 Aug 2011 17:26:30 +0100 Subject: new completion semantics this time it respects suffixes and can complete recipients from your addressbook --- alot/account.py | 1 - alot/command.py | 11 ++-- alot/completion.py | 147 ++++++++++++++++++++++++++++++++++------------------- alot/widgets.py | 33 ++++++------ 4 files changed, 115 insertions(+), 77 deletions(-) diff --git a/alot/account.py b/alot/account.py index d4bc0673..f0e68fc2 100644 --- a/alot/account.py +++ b/alot/account.py @@ -192,7 +192,6 @@ class AccountManager: for s in accountsections: options = filter(lambda x: x in self.allowed, config.options(s)) - args = {} if 'abook_command' in options: cmd = config.get(s, 'abook_command').encode('ascii', diff --git a/alot/command.py b/alot/command.py index 1c58e370..41604eb6 100644 --- a/alot/command.py +++ b/alot/command.py @@ -703,7 +703,6 @@ class PrintCommand(Command): ui.notify(ok_msg) - class SaveAttachmentCommand(Command): def __init__(self, all=False, path=None, **kwargs): Command.__init__(self, **kwargs) @@ -806,9 +805,9 @@ class EnvelopeEditCommand(Command): def openEnvelopeFromTmpfile(): # This parses the input from the tempfile. - # we do this ourselves here because we want to be able to - # just type utf-8 encoded stuff into the tempfile and let alot worry - # about encodings. + # we do this ourselves here because we want to be able to + # just type utf-8 encoded stuff into the tempfile and let alot + # worry about encodings. # get input f = open(tf.name) @@ -819,12 +818,12 @@ class EnvelopeEditCommand(Command): # go through multiline, utf-8 encoded headers key = value = None for line in headertext.splitlines(): - if re.match('\w+:', line): #new k/v pair + if re.match('\w+:', line): # new k/v pair if key and value: # save old one from stack del self.mail[key] # ensure unique values in mails self.mail[key] = encode_header(key, value) # save key, value = line.strip().split(':', 1) # parse new pair - elif key and value: # append new line without key prefix to value + elif key and value: # append new line without key prefix value += line if key and value: # save last one if present del self.mail[key] diff --git a/alot/completion.py b/alot/completion.py index 27e81bdc..3cc673aa 100644 --- a/alot/completion.py +++ b/alot/completion.py @@ -19,21 +19,37 @@ Copyright (C) 2011 Patrick Totzke import re import os +import glob import logging import command class Completer: - def complete(self, original): - """takes a string that's the prefix of a word, - returns a list of suffix-strings that complete the original""" + def complete(self, original, pos): + """returns a list of completions and cursor positions for the + string original from position pos on. + + :param original: the complete string to complete + :type original: str + :param pos: starting position to complete from + :returns: a list of tuples (ctext, cpos), where ctext is the completed + string and cpos the cursor position in the new string + """ return list() + def relevant_part(self, original, pos, sep=' '): + """calculates the subword in a sep-splitted list of original + that pos is in""" + start = original.rfind(sep, 0, pos) + 1 + end = original.find(sep, pos - 1) + if end == -1: + end = len(original) + return original[start:end], start, end, pos - start + class QueryCompleter(Completer): """completion for a notmuch query string""" - # TODO: boolean connectors and braces? def __init__(self, dbman, accountman): self.dbman = dbman self._contactscompleter = ContactsCompleter(accountman, @@ -42,19 +58,32 @@ class QueryCompleter(Completer): self.keywords = ['tag', 'from', 'to', 'subject', 'attachment', 'is', 'id', 'thread', 'folder'] - def complete(self, original): - prefix = original.split(' ')[-1] - m = re.search('(tag|is|to):(\w*)', prefix) + def complete(self, original, pos): + mypart, start, end, mypos = self.relevant_part(original, pos) + myprefix = mypart[:mypos] + m = re.search('(tag|is|to):(\w*)', myprefix) if m: cmd, params = m.groups() + cmdlen = len(cmd) + 1 # length of the keyword part incld colon if cmd == 'to': - return self._contactscompleter.complete(params) + localres = self._contactscompleter.complete(mypart[cmdlen:], + mypos - cmdlen) else: - return self._tagscompleter.complete(params, last=True) + localres = self._tagscompleter.complete(mypart[cmdlen:], + mypos - cmdlen) + resultlist = [] + for ltxt, lpos in localres: + newtext = original[:start] + cmd + ':' + ltxt + original[end:] + newpos = start + len(cmd) + 1 + lpos + resultlist.append((newtext, newpos)) + return resultlist else: - plen = len(prefix) - matched = filter(lambda t: t.startswith(prefix), self.keywords) - return [t + ':' for t in matched] + matched = filter(lambda t: t.startswith(myprefix), self.keywords) + resultlist = [] + for keyword in matched: + newprefix = original[:start] + keyword + ':' + resultlist.append((newprefix + original[end:], len(newprefix))) + return resultlist class TagsCompleter(Completer): @@ -63,18 +92,24 @@ class TagsCompleter(Completer): def __init__(self, dbman): self.dbman = dbman - def complete(self, original, last=False): - otags = original.split(',') - prefix = otags[-1] + def complete(self, original, pos, single_tag=True): tags = self.dbman.get_all_tags() - if len(otags) > 1 and last: - return [] - else: + if single_tag: + prefix = original[:pos] matching = [t for t in tags if t.startswith(prefix)] - if last: - return matching - else: - return [t + ',' for t in matching] + return [(t, len(t)) for t in matching] + else: + mypart, start, end, mypos = self.relevant_part(original, pos, + sep=',') + prefix = mypart[:mypos] + res = [] + for tag in tags: + if tag.startswith(prefix): + newprefix = original[:start] + tag + if not original[end:].startswith(','): + newprefix += ',' + res.append((newprefix + original[end:], len(newprefix))) + return res class ContactsCompleter(Completer): @@ -83,17 +118,20 @@ class ContactsCompleter(Completer): self.abooks = accountman.get_addressbooks() self.addressesonly = addressesonly - def complete(self, prefix): + def complete(self, original, pos): if not self.abooks: return [] + prefix = original[:pos] res = [] for abook in self.abooks: res = res + abook.lookup(prefix) - logging.debug(res) if self.addressesonly: - returnlist = [e for n,e in res] + returnlist = [(email, len(email)) for (name, email) in res] else: - returnlist = ["%s <%s>" % x for x in res] + returnlist = [] + for name, email in res: + newtext = "%s <%s>" % (name, email) + returnlist.append((newtext, len(newtext))) return returnlist @@ -103,9 +141,10 @@ class AccountCompleter(Completer): def __init__(self, accountman): self.accountman = accountman - def complete(self, prefix): + def complete(self, original, pos): valids = self.accountman.get_main_addresses() - return [a for a in valids if a.startswith(prefix)] + prefix = original[:pos] + return [(a, len(a)) for a in valids if a.startswith(prefix)] class CommandCompleter(Completer): @@ -115,12 +154,14 @@ class CommandCompleter(Completer): self.dbman = dbman self.mode = mode - def complete(self, original): + def complete(self, original, pos): #TODO refine should get current querystring + commandprefix = original[:pos] + logging.debug('original="%s" prefix="%s"' % (original, commandprefix)) cmdlist = command.COMMANDS['global'] cmdlist.update(command.COMMANDS[self.mode]) - olen = len(original) - return [t + '' for t in cmdlist if t.startswith(original)] + matching = [t for t in cmdlist if t.startswith(commandprefix)] + return [(t, len(t)) for t in matching] class CommandLineCompleter(Completer): @@ -136,36 +177,36 @@ class CommandLineCompleter(Completer): self._contactscompleter = ContactsCompleter(accountman) self._pathcompleter = PathCompleter() - def complete(self, prefix): - words = prefix.split(' ', 1) - if len(words) <= 1: # we complete commands - return self._commandcompleter.complete(prefix) + def complete(self, line, pos): + words = line.split(' ', 1) + + res = [] + if pos <= len(words[0]): # we complete commands + for cmd, cpos in self._commandcompleter.complete(line, pos): + newtext = ('%s %s' % (cmd, ' '.join(words[1:]))) + res.append((newtext, cpos + 1)) else: cmd, params = words + localpos = pos - (len(cmd) + 1) if cmd in ['search', 'refine']: - return self._querycompleter.complete(params) + res = self._querycompleter.complete(params, localpos) if cmd == 'retag': - return self._tagscompleter.complete(params) + res = self._tagscompleter.complete(params, localpos, + single_tag=False) if cmd == 'toggletag': - return self._tagscompleter.complete(params, last=True) + res = self._tagscompleter.complete(params, localpos) if cmd in ['to', 'compose']: - return self._contactscompleter.complete(params) + res = self._contactscompleter.complete(params, localpos) if cmd in ['attach', 'edit', 'save']: - return self._pathcompleter.complete(params) - else: - return [] + res = self._pathcompleter.complete(params, localpos) + res = [('%s %s' % (cmd, t), p + len(cmd) + 1) for (t, p) in res] + return res class PathCompleter(Completer): """completion for paths""" - def complete(self, prefix): - if not prefix: - return ['~/'] - dir = os.path.expanduser(os.path.dirname(prefix)) - fileprefix = os.path.basename(prefix) - res = [] - if os.path.isdir(dir): - for f in os.listdir(dir): - if f.startswith(fileprefix): - res.append(f) - return res + def complete(self, original, pos): + if not original: + return [('~/', 2)] + prefix = os.path.expanduser(original[:pos]) + return [(f, len(f)) for f in glob.glob(prefix + '*')] diff --git a/alot/widgets.py b/alot/widgets.py index 02cf6acb..4fe7439b 100644 --- a/alot/widgets.py +++ b/alot/widgets.py @@ -172,37 +172,36 @@ class CompleteEdit(urwid.Edit): if not isinstance(edit_text, unicode): edit_text = unicode(edit_text, errors='replace') self.start_completion_pos = len(edit_text) - self.completion_results = None + self.completions = None urwid.Edit.__init__(self, edit_text=edit_text, **kwargs) def keypress(self, size, key): cmd = command_map[key] + # if we tabcomplete if cmd in ['next selectable', 'prev selectable']: - pos = self.start_completion_pos - original = self.edit_text[:pos] - if not self.completion_results: # not in completion mode - self.completion_results = [''] + \ - self.completer.complete(original) + # if not already in completion mode + if not self.completions: + self.completions = [(self.edit_text, self.edit_pos)] + \ + self.completer.complete(self.edit_text, self.edit_pos) self.focus_in_clist = 1 - else: + else: # otherwise tab through results if cmd == 'next selectable': self.focus_in_clist += 1 else: self.focus_in_clist -= 1 - if len(self.completion_results) > 1: - completed = self.completion_results[self.focus_in_clist % - len(self.completion_results)] - self.set_edit_text(completed) - self.edit_pos += len(completed) + if len(self.completions) > 1: + ctext, cpos = self.completions[self.focus_in_clist % + len(self.completions)] + self.set_edit_text(ctext) + self.set_edit_pos(cpos) else: - self.set_edit_text(original + ' ') self.edit_pos += 1 - self.start_completion_pos = self.edit_pos - self.completion_results = None + if self.edit_pos >= len(self.edit_text): + self.edit_text += ' ' + self.completions = None else: result = urwid.Edit.keypress(self, size, key) - self.start_completion_pos = self.edit_pos - self.completion_results = None + self.completions = None return result -- cgit v1.2.3 From fa295ef4b7cf1e1454c3b9a0574d9793527e9b92 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Tue, 30 Aug 2011 12:41:48 +0100 Subject: fix no-results with abook lookup issue #42 --- alot/account.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/alot/account.py b/alot/account.py index f0e68fc2..0447d0a8 100644 --- a/alot/account.py +++ b/alot/account.py @@ -201,7 +201,7 @@ class AccountManager: rgexp = config.get(s, 'abook_regexp') options.remove('abook_regexp') else: - regexp = None + regexp = None # will use default in constructor args['abook'] = MatchSdtoutAddressbook(cmd, match=regexp) to_set = self.manditory @@ -296,8 +296,10 @@ class MatchSdtoutAddressbook(AddressBook): return self.lookup('\'\'') def lookup(self, prefix): - lines = cmd_output('%s %s' % (self.command, prefix)) - lines = lines.replace('\t', ' ' * 4).splitlines() + resultstring = cmd_output('%s %s' % (self.command, prefix)) + if not resultstring: + return [] + lines = resultstring.replace('\t', ' ' * 4).splitlines() res = [] for l in lines: m = re.match(self.match, l) -- cgit v1.2.3 From b65d427c2af72d1894bb1d465cb61325e28c732c Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Tue, 30 Aug 2011 12:52:45 +0100 Subject: more sane defaults for printing: issue #45: global.print_cmd defaults to none, PrintCommand handles it accordingly. --- alot/command.py | 19 +++++++++++++------ alot/settings.py | 2 +- data/example.full.rc | 7 +++++-- data/example.rc | 3 ++- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/alot/command.py b/alot/command.py index 41604eb6..67912f0a 100644 --- a/alot/command.py +++ b/alot/command.py @@ -661,16 +661,26 @@ class PrintCommand(Command): self.confirm = confirm def apply(self, ui): - # get messages to print + # get print command and abort if unset + cmd = settings.config.get('general', 'print_cmd') + if not cmd: + ui.notify('no print command specified.\n' + 'set "print_cmd" in the global section.', + priority='error') + return + args = shlex.split(cmd.encode('ascii')) + + # get messages to print and set up notification strings if self.all: thread = ui.current_buffer.get_selected_thread() to_print = thread.get_messages().keys() confirm_msg = 'print all messages in thread?' - ok_msg = 'printed thread: %s' % str(thread) + ok_msg = 'printed thread: %s using %s' % (str(thread), cmd) else: to_print = [ui.current_buffer.get_selected_message()] confirm_msg = 'print this message?' - ok_msg = 'printed message: %s' % str(to_print[0]) + ok_msg = 'printed message: %s using %s' % (str(to_print[0]), cmd) + # ask for confirmation if needed if self.confirm: @@ -682,9 +692,6 @@ class PrintCommand(Command): if not self.separately: mailstrings = ['\n\n'.join(mailstrings)] - # get print command - cmd = settings.config.get('general', 'print_cmd') - args = shlex.split(cmd.encode('ascii')) # print try: diff --git a/alot/settings.py b/alot/settings.py index ef3babff..a09923d5 100644 --- a/alot/settings.py +++ b/alot/settings.py @@ -41,7 +41,7 @@ DEFAULTS = { 'hooksfile': '~/.alot.py', 'bug_on_exit': 'False', 'timestamp_format': '', - 'print_cmd': 'muttprint', + 'print_cmd': '', }, '16c-theme': { 'bufferlist_focus_bg': 'dark gray', diff --git a/data/example.full.rc b/data/example.full.rc index 71ae8301..bf07bd06 100644 --- a/data/example.full.rc +++ b/data/example.full.rc @@ -44,8 +44,11 @@ terminal_cmd = x-terminal-emulator -e # http://docs.python.org/library/datetime.html#strftime-strptime-behavior timestamp_format = '' -#how to print messages: -print_cmd = muttprint +# how to print messages: +# this specifies a shellcommand used pro printing. +# threads/messages are piped to this as plaintext. +# muttprint/a2ps works nicely +print_cmd = [global-maps] diff --git a/data/example.rc b/data/example.rc index 699bfd53..897ab4be 100644 --- a/data/example.rc +++ b/data/example.rc @@ -2,7 +2,6 @@ colourmode = 256 ; number of colours your terminal supports hooksfile = hooks.py editor_cmd = /usr/bin/vim -f -c 'set filetype=mail' -pager_cmd = /usr/bin/view -f -c 'set filetype=mail' terminal_cmd = /usr/bin/urxvt -T notmuch -e spawn_editor = False authors_maxlength = 30 @@ -10,6 +9,7 @@ displayed_headers = From,To,Cc,Bcc,Subject ask_subject = True notify_timeout = 1 # how long (in seconds) notifications are shown flush_retry_timeout = 5 # timeout in secs after a failed attempt to flush is repeated +print_cmd = muttprint [account gmail] realname = john doe @@ -21,6 +21,7 @@ signature_filename = sig.txt # filename in attachment part sendmail_command = msmtp --account=gmail -t sent_box = maildir:///home/john/mail/gmail/Sent # we accept mbox,maildir,mh,babyl,mmdf here draft_box = maildir:///home/john/mail/gmail/Drafts # we accept mbox,maildir,mh,babyl,mmdf here +abook_command = abook --mutt-query [search-maps] t = toggletag todo -- cgit v1.2.3 From d4c67504b31d8a4c6a89f02425f50c28970fcd43 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Tue, 30 Aug 2011 13:13:54 +0100 Subject: unifies pipe to cmd in send_mail and print. issue #39 --- alot/account.py | 20 ++++++-------------- alot/command.py | 18 +++++------------- alot/helper.py | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/alot/account.py b/alot/account.py index 0447d0a8..dcf971b4 100644 --- a/alot/account.py +++ b/alot/account.py @@ -29,6 +29,7 @@ from ConfigParser import SafeConfigParser from urlparse import urlparse from helper import cmd_output +import helper class Account: @@ -151,20 +152,11 @@ class SendmailAccount(Account): def send_mail(self, mail): mail['Date'] = email.utils.formatdate(time.time(), True) - # no unicode in shlex on 2.x - args = shlex.split(self.cmd.encode('ascii')) - try: - proc = subprocess.Popen(args, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out, err = proc.communicate(mail.as_string()) - except OSError, e: - return str(e) + '. sendmail_cmd set to: %s' % self.cmd - if proc.poll(): # returncode is not 0 - return err.strip() - else: - self.store_sent_mail(mail) - return None + out, err = helper.pipe_to_command(self.cmd, mail.as_string()) + if err: + return err + '. sendmail_cmd set to: %s' % self.cmd + self.store_sent_mail(mail) + return None class AccountManager: diff --git a/alot/command.py b/alot/command.py index 67912f0a..612bf0bf 100644 --- a/alot/command.py +++ b/alot/command.py @@ -668,7 +668,6 @@ class PrintCommand(Command): 'set "print_cmd" in the global section.', priority='error') return - args = shlex.split(cmd.encode('ascii')) # get messages to print and set up notification strings if self.all: @@ -692,19 +691,12 @@ class PrintCommand(Command): if not self.separately: mailstrings = ['\n\n'.join(mailstrings)] - # print - try: - for mail in mailstrings: - proc = subprocess.Popen(args, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out, err = proc.communicate(mail) - if proc.poll(): # returncode is not 0 - raise OSError(err) - except OSError, e: # handle errors - ui.notify(str(e), priority='error') - return + for mail in mailstrings: + out, err = helper.pipe_to_command(cmd, mail) + if err: + ui.notify(err, priority='error') + return # display 'done' message ui.notify(ok_msg) diff --git a/alot/helper.py b/alot/helper.py index 72c9cb63..3516d77e 100644 --- a/alot/helper.py +++ b/alot/helper.py @@ -61,6 +61,22 @@ def cmd_output(command_line): return output +def pipe_to_command(cmd, stdin): + # no unicode in shlex on 2.x + args = shlex.split(cmd.encode('ascii')) + try: + proc = subprocess.Popen(args, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = proc.communicate(stdin) + except OSError, e: + return '', str(e) + if proc.poll(): # returncode is not 0 + return '', err.strip() + else: + return out, err + + def attach(path, mail, filename=None): ctype, encoding = mimetypes.guess_type(path) if ctype is None or encoding is not None: -- cgit v1.2.3 From 7f9743305bd9e2c343e2441bd1151fb77b482fe4 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Tue, 30 Aug 2011 13:17:39 +0100 Subject: issue #40: call thread.refresh upon redraw --- alot/buffer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/alot/buffer.py b/alot/buffer.py index eb476530..dc37d5e2 100644 --- a/alot/buffer.py +++ b/alot/buffer.py @@ -206,6 +206,7 @@ class ThreadBuffer(Buffer): self._build_pile(acc, reply, msg, depth + 1) def rebuild(self): + self.thread.refresh() # depth-first traversing the thread-tree, thereby # 1) build a list of tuples (parentmsg, depth, message) in DF order # 2) create a dict that counts no. of direct replies per message -- cgit v1.2.3 From 7777201c6c0308c07d7d3bade24eb4094f36650b Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Tue, 30 Aug 2011 13:28:17 +0100 Subject: pep8 --- USAGE | 9 +++++++++ alot/account.py | 1 - alot/command.py | 1 - 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/USAGE b/USAGE index e537956b..5efcec4a 100644 --- a/USAGE +++ b/USAGE @@ -107,6 +107,8 @@ I use this for my uni-account: draft_box = maildir:///home/pazz/mail/uoe/Drafts signature = ~/my_uni_vcard.vcs signature_filename = p.totzke.vcs + abook_command = abook --mutt-query + Caution: Sending mails is only supported via sendmail for now. If you want to use a sendmail command different from `sendmail`, specify it as `sendmail_command`. @@ -118,6 +120,13 @@ in the protocol part of the url. The file specified by `signature` is attached to all outgoing mails from this account, optionally renamed to `signature_filename`. +If you specified `abook_command`, it will be used for tab completion in queries (to/from) +and in message composition. The command will be called with your prefix as only argument +and its output is searched for name-email pairs. The regular expression used here +defaults to `(?P.+?@.+?)\s+(?P.+)`, which makes it work nicely with `abook --mutt-query`. +You can tune this using the `abook_regexp` option (beware Commandparsers escaping semantic!). + + Hooks ----- Before and after every command execution, alot calls this commands pre/post hook: diff --git a/alot/account.py b/alot/account.py index dcf971b4..014ed087 100644 --- a/alot/account.py +++ b/alot/account.py @@ -276,7 +276,6 @@ class AbookAddressBook(AddressBook): class MatchSdtoutAddressbook(AddressBook): - def __init__(self, command, match=None): self.command = command if not match: diff --git a/alot/command.py b/alot/command.py index 612bf0bf..4f40b85a 100644 --- a/alot/command.py +++ b/alot/command.py @@ -680,7 +680,6 @@ class PrintCommand(Command): confirm_msg = 'print this message?' ok_msg = 'printed message: %s using %s' % (str(to_print[0]), cmd) - # ask for confirmation if needed if self.confirm: if not ui.choice(confirm_msg) == 'yes': -- cgit v1.2.3 From 4420c123e1d0c564b81cd455bbcc52d87597800b Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Thu, 1 Sep 2011 16:08:47 +0100 Subject: fix buf with non-basename filenames in attachments Apparently, some clients (k9 on android) send attachments with a full pathname as filename. in that case we only use its basename. --- alot/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alot/message.py b/alot/message.py index f3128eba..85673858 100644 --- a/alot/message.py +++ b/alot/message.py @@ -296,7 +296,7 @@ class Attachment: def get_filename(self): """return the filename, extracted from content-disposition header""" - return self.part.get_filename() + return os.path.basename(self.part.get_filename()) def get_content_type(self): """mime type of the attachment""" -- cgit v1.2.3 From f1dd0703a18719e879e9d79d8a40dfc3d21d0273 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 4 Sep 2011 19:21:44 +0100 Subject: fix: forgot to change call to renamed method --- alot/buffer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alot/buffer.py b/alot/buffer.py index dc37d5e2..1ea7766b 100644 --- a/alot/buffer.py +++ b/alot/buffer.py @@ -283,7 +283,7 @@ class TagListBuffer(Buffer): displayedtags = filter(self.filtfun, self.tags) for (num, b) in enumerate(displayedtags): tw = widgets.TagWidget(b) - lines.append(urwid.Columns([('fixed', tw.len(), tw)])) + lines.append(urwid.Columns([('fixed', tw.width(), tw)])) self.taglist = urwid.ListBox(urwid.SimpleListWalker(lines)) self.body = self.taglist -- cgit v1.2.3 From a00c88813829f5c42b5ee881f3f04c3f3885e68a Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 4 Sep 2011 20:04:57 +0100 Subject: refactored PrintCommand to PipeCommand --- alot/command.py | 63 ++++++++++++++++++++++++++++++++++++++------------------- alot/helper.py | 5 ++++- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/alot/command.py b/alot/command.py index 4f40b85a..227004b5 100644 --- a/alot/command.py +++ b/alot/command.py @@ -653,36 +653,34 @@ class ToggleHeaderCommand(Command): msgw.toggle_full_header() -class PrintCommand(Command): - def __init__(self, all=False, separately=False, confirm=True, **kwargs): +class PipeCommand(Command): + def __init__(self, cmd, whole_thread=False, separately=False, + noop_msg='no command specified', confirm_msg='', done_msg='', + **kwargs): Command.__init__(self, **kwargs) - self.all = all + self.cmd = cmd + self.whole_thread = whole_thread self.separately = separately - self.confirm = confirm + self.noop_msg = noop_msg + self.confirm_msg = confirm_msg + self.done_msg = done_msg def apply(self, ui): - # get print command and abort if unset - cmd = settings.config.get('general', 'print_cmd') - if not cmd: - ui.notify('no print command specified.\n' - 'set "print_cmd" in the global section.', - priority='error') + # abort if command unset + if not self.cmd: + ui.notify(self.noop_msg, priority='error') return - # get messages to print and set up notification strings - if self.all: + # get messages to pipe + if self.whole_thread: thread = ui.current_buffer.get_selected_thread() to_print = thread.get_messages().keys() - confirm_msg = 'print all messages in thread?' - ok_msg = 'printed thread: %s using %s' % (str(thread), cmd) else: to_print = [ui.current_buffer.get_selected_message()] - confirm_msg = 'print this message?' - ok_msg = 'printed message: %s using %s' % (str(to_print[0]), cmd) # ask for confirmation if needed - if self.confirm: - if not ui.choice(confirm_msg) == 'yes': + if self.confirm_msg: + if not ui.choice(self.confirm_msg) == 'yes': return # prepare message sources @@ -692,13 +690,36 @@ class PrintCommand(Command): # print for mail in mailstrings: - out, err = helper.pipe_to_command(cmd, mail) + out, err = helper.pipe_to_command(self.cmd, mail) if err: ui.notify(err, priority='error') return # display 'done' message - ui.notify(ok_msg) + if self.done_msg: + ui.notify(self.done_msg) + + +class PrintCommand(PipeCommand): + def __init__(self, whole_thread=False, separately=False, **kwargs): + # get print command + cmd = settings.config.get('general', 'print_cmd', fallback='') + + # set up notification strings + if whole_thread: + confirm_msg = 'print all messages in thread?' + ok_msg = 'printed thread using %s' % cmd + else: + confirm_msg = 'print selected message?' + ok_msg = 'printed message using %s' % cmd + + # no print cmd set + noop_msg = 'no print command specified. Set "print_cmd" in the '\ + 'global section.' + PipeCommand.__init__(self, cmd, whole_thread=whole_thread, + separately=separately, + noop_msg=noop_msg, confirm_msg=confirm_msg, + done_msg=ok_msg, **kwargs) class SaveAttachmentCommand(Command): @@ -1108,7 +1129,7 @@ def interpret_commandline(cmdline, mode): return commandfactory(cmd, mode=mode, path=filepath) elif cmd == 'print': args = [a.strip() for a in params.split()] - return commandfactory(cmd, mode=mode, all=('--all' in args), + return commandfactory(cmd, mode=mode, whole_thread=('--all' in args), separately=('--separately' in args)) elif not params and cmd in ['exit', 'flush', 'pyshell', 'taglist', diff --git a/alot/helper.py b/alot/helper.py index 3516d77e..d20ba844 100644 --- a/alot/helper.py +++ b/alot/helper.py @@ -72,7 +72,10 @@ def pipe_to_command(cmd, stdin): except OSError, e: return '', str(e) if proc.poll(): # returncode is not 0 - return '', err.strip() + e = 'return value != 0' + if err.strip(): + e = e + ': %s' % err + return '', e else: return out, err -- cgit v1.2.3 From 4b77957e590cdb16e34e734a80ad7faad153b8d6 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 4 Sep 2011 20:36:08 +0100 Subject: interpret 'pipeto' command, mapped to | in thread --- USAGE | 1 + alot/command.py | 14 +++++++++----- alot/settings.py | 3 ++- data/example.full.rc | 2 ++ 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/USAGE b/USAGE index 5efcec4a..8db4a62f 100644 --- a/USAGE +++ b/USAGE @@ -54,6 +54,7 @@ in the config file. These are the default keymaps: g = groupreply p = print r = reply + | = prompt pipeto Config ------ diff --git a/alot/command.py b/alot/command.py index 227004b5..3111f2cd 100644 --- a/alot/command.py +++ b/alot/command.py @@ -654,11 +654,11 @@ class ToggleHeaderCommand(Command): class PipeCommand(Command): - def __init__(self, cmd, whole_thread=False, separately=False, - noop_msg='no command specified', confirm_msg='', done_msg='', + def __init__(self, command, whole_thread=False, separately=False, + noop_msg='no command specified', confirm_msg='', done_msg='done', **kwargs): Command.__init__(self, **kwargs) - self.cmd = cmd + self.cmd = command self.whole_thread = whole_thread self.separately = separately self.noop_msg = noop_msg @@ -688,7 +688,7 @@ class PipeCommand(Command): if not self.separately: mailstrings = ['\n\n'.join(mailstrings)] - # print + # do teh monkey for mail in mailstrings: out, err = helper.pipe_to_command(self.cmd, mail) if err: @@ -1011,6 +1011,7 @@ COMMANDS = { 'groupreply': (ReplyCommand, {'groupreply': True}), 'forward': (ForwardCommand, {}), 'fold': (FoldMessagesCommand, {'visible': False}), + 'pipeto': (PipeCommand, {}), 'print': (PrintCommand, {}), 'unfold': (FoldMessagesCommand, {'visible': True}), 'select': (ThreadSelectCommand, {}), @@ -1059,6 +1060,7 @@ def commandfactory(cmdname, mode='global', **kwargs): def interpret_commandline(cmdline, mode): + # TODO: use argparser here! if not cmdline: return None logging.debug('mode:%s got commandline "%s"' % (mode, cmdline)) @@ -1129,8 +1131,10 @@ def interpret_commandline(cmdline, mode): return commandfactory(cmd, mode=mode, path=filepath) elif cmd == 'print': args = [a.strip() for a in params.split()] - return commandfactory(cmd, mode=mode, whole_thread=('--all' in args), + return commandfactory(cmd, mode=mode, whole_thread=('--thread' in args), separately=('--separately' in args)) + elif cmd == 'pipeto': + return commandfactory(cmd, mode=mode, command=params) elif not params and cmd in ['exit', 'flush', 'pyshell', 'taglist', 'bclose', 'compose', 'openfocussed', diff --git a/alot/settings.py b/alot/settings.py index a09923d5..25eb1a43 100644 --- a/alot/settings.py +++ b/alot/settings.py @@ -243,13 +243,14 @@ DEFAULTS = { 'C': 'fold --all', 'E': 'unfold --all', 'H': 'toggleheaders', - 'P': 'print --all', + 'P': 'print --thread', 'a': 'toggletag inbox', 'enter': 'select', 'f': 'forward', 'g': 'groupreply', 'p': 'print', 'r': 'reply', + '|': 'prompt pipeto ', }, 'taglist-maps': { 'enter': 'select', diff --git a/data/example.full.rc b/data/example.full.rc index bf07bd06..d343735b 100644 --- a/data/example.full.rc +++ b/data/example.full.rc @@ -101,6 +101,8 @@ f = forward g = groupreply p = print r = reply +| = prompt pipeto + [command-aliases] bn = bnext -- cgit v1.2.3 From 6e53cb6a413e5bca2119eedc697f6cd3f9cccf51 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Tue, 6 Sep 2011 16:13:02 +0100 Subject: fix issue with os.basename of NoneType filenames --- alot/message.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/alot/message.py b/alot/message.py index 85673858..5d9c3fd2 100644 --- a/alot/message.py +++ b/alot/message.py @@ -296,7 +296,10 @@ class Attachment: def get_filename(self): """return the filename, extracted from content-disposition header""" - return os.path.basename(self.part.get_filename()) + extracted_name = self.part.get_filename() + if extracted_name: + return os.path.basename(extracted_name) + return None def get_content_type(self): """mime type of the attachment""" @@ -314,6 +317,7 @@ class Attachment: return "%dK" % size_in_kbyte def save(self, path): + # todo: raise exception if path not dir """save the attachment to disk. Uses self.get_filename in case path is a directory""" if self.get_filename() and os.path.isdir(path): -- cgit v1.2.3 From 9eaabcd12177984dd25dba56ff82928f6303c5a5 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Thu, 8 Sep 2011 03:47:05 +0200 Subject: Add config option for initial searchstring New option for the alot.rc initial_searchstring where can be defined the default search string when alot is opend without params. --- alot/init.py | 9 +++++++-- alot/settings.py | 1 + data/example.full.rc | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/alot/init.py b/alot/init.py index e5a13a5b..9972e42f 100755 --- a/alot/init.py +++ b/alot/init.py @@ -52,7 +52,7 @@ def parse_args(): default='/dev/null', help='logfile') parser.add_argument('query', nargs='?', - default='tag:inbox AND NOT tag:killed', + default='', help='initial searchstring') return parser.parse_args() @@ -87,11 +87,16 @@ def main(): command_map['enter'] = 'select' command_map['esc'] = 'cancel' + # get initial searchstring + query = settings.config.get('general','initial_searchstring') + if args.query != '': + query = args.query + # set up and start interface ui = UI(dbman, logger, aman, - args.query, + query, args.colours, ) diff --git a/alot/settings.py b/alot/settings.py index 25eb1a43..18a3cfd5 100644 --- a/alot/settings.py +++ b/alot/settings.py @@ -42,6 +42,7 @@ DEFAULTS = { 'bug_on_exit': 'False', 'timestamp_format': '', 'print_cmd': '', + 'initial_searchstring': 'tag:inbox AND NOT tag:killed', }, '16c-theme': { 'bufferlist_focus_bg': 'dark gray', diff --git a/data/example.full.rc b/data/example.full.rc index d343735b..124db49c 100644 --- a/data/example.full.rc +++ b/data/example.full.rc @@ -50,6 +50,9 @@ timestamp_format = '' # muttprint/a2ps works nicely print_cmd = +#initial searchstring when none is given as argument: +initial_searchstring = tag:inbox AND NOT tag:killed + [global-maps] $ = flush -- cgit v1.2.3 From 1852d09117e367517509613ed9c81b90ff2a8cbd Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Thu, 8 Sep 2011 10:36:05 +0100 Subject: updated readme --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6e8ca745..fca69be2 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ the `docs` directory contains their sources. Do comment on the code or file issues! I'm curious what you think of it. You can talk to me in #notmuch@Freenode. +Be aware that the master branch is used only for releases and hotfixes, +the bleeding edge version sits in branch `development`!. +If you'd like to contribute, please make sure your patches can be applied to that branch. Current features include: ------------------------- @@ -23,7 +26,8 @@ Current features include: * priorizable notification popups * database manager that manages a write queue to the notmuch index * user configurable keyboard maps - * printing + * printing/piping of mails and threads + * addressbook integration (dev branch) Soonish to be addressed non-features: ------------------------------------- @@ -31,7 +35,6 @@ Soonish to be addressed non-features: * search for strings in displayed buffer * folding for message parts * undo for commands - * addressbook integration [notmuch]: http://notmuchmail.org/ [urwid]: http://excess.org/urwid/ -- cgit v1.2.3 From 7ed4de69f2aa95335d8c97009db8ce9f4ddad31e Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sat, 10 Sep 2011 19:53:15 +0100 Subject: twisted mainloop and non-blocking prompt issue #51 --- alot/ui.py | 93 ++++++++++++++++++++++++++------------------------------------ 1 file changed, 39 insertions(+), 54 deletions(-) diff --git a/alot/ui.py b/alot/ui.py index 74d5aa51..1ee366f1 100644 --- a/alot/ui.py +++ b/alot/ui.py @@ -19,6 +19,7 @@ Copyright (C) 2011 Patrick Totzke import urwid import os from urwid.command_map import command_map +from twisted.internet import defer from settings import config from buffer import BufferlistBuffer @@ -30,7 +31,7 @@ from completion import CommandLineCompleter class MainWidget(urwid.Frame): def __init__(self, ui, *args, **kwargs): - urwid.Frame.__init__(self, urwid.SolidFill(' '), *args, **kwargs) + urwid.Frame.__init__(self, urwid.SolidFill(), *args, **kwargs) self.ui = ui def keypress(self, size, key): @@ -61,6 +62,7 @@ class UI: self.mainloop = urwid.MainLoop(self.mainframe, config.get_palette(), handle_mouse=False, + event_loop=urwid.TwistedEventLoop(), unhandled_input=self.keypress) self.mainloop.screen.set_terminal_properties(colors=colourmode) @@ -91,59 +93,41 @@ class UI: :type tab: int :param history: history to be used for up/down keys :type history: list of str + :returns: a `twisted.defer.Deferred` """ - self.logger.info('open prompt') - history = list(history) # make a local copy - historypos = None + d = defer.Deferred() # create return deferred + main = self.mainloop.widget # save main widget + + def select_or_cancel(text): + self.mainloop.widget = main # restore main screen + d.callback(text) + + #set up widgets leftpart = urwid.Text(prefix, align='left') - if completer: - editpart = CompleteEdit(completer, edit_text=text) - for i in range(tab): - editpart.keypress((0,), 'tab') - else: - editpart = urwid.Edit(edit_text=text) + editpart = CompleteEdit(completer, on_exit=select_or_cancel, + edit_text=text, history=history) + + for i in range(tab): # hit some tabs + editpart.keypress((0,), 'tab') + + # build promptwidget both = urwid.Columns( [ ('fixed', len(prefix), leftpart), ('weight', 1, editpart), ]) prompt_widget = urwid.AttrMap(both, 'prompt', 'prompt') - footer = self.mainframe.get_footer() - self.mainframe.set_footer(prompt_widget) - self.mainframe.set_focus('footer') - self.mainloop.draw_screen() - while True: - keys = None - while not keys: - keys = self.mainloop.screen.get_input() - for key in keys: - self.logger.debug('prompt got key: %s' % key) - if command_map[key] == 'select': - self.mainframe.set_footer(footer) - self.mainframe.set_focus('body') - return editpart.get_edit_text() - elif command_map[key] == 'cancel': - self.mainframe.set_footer(footer) - self.mainframe.set_focus('body') - return None - elif key in ['up', 'down']: - if history: - if historypos == None: - history.append(editpart.get_edit_text()) - historypos = len(history) - 1 - if key == 'cursor up': - historypos = (historypos - 1) % len(history) - else: - historypos = (historypos + 1) % len(history) - editpart.set_edit_text(history[historypos]) - self.mainloop.draw_screen() - - else: - size = (20,) # don't know why they want a size here - editpart.keypress(size, key) - self.mainloop.draw_screen() + # put promptwidget as overlay on main widget + overlay = urwid.Overlay(both, main, + ('fixed left', 0), + ('fixed right', 0), + ('fixed bottom', 1), + None) + self.mainloop.widget = overlay + return d # return deferred + @defer.inlineCallbacks def commandprompt(self, startstring): """prompt for a commandline and interpret/apply it upon enter @@ -152,15 +136,15 @@ class UI: """ self.logger.info('open command shell') mode = self.current_buffer.typename - cmdline = self.prompt(prefix=':', + cmdline = yield self.prompt(prefix=':', text=startstring, completer=CommandLineCompleter(self.dbman, self.accountman, mode), history=self.commandprompthistory, ) - if cmdline: - self.interpret_commandline(cmdline) + self.logger.debug('CMDLINE: %s' % cmdline) + self.interpret_commandline(cmdline) def interpret_commandline(self, cmdline): """interpret and apply a commandstring @@ -168,13 +152,14 @@ class UI: :param cmdline: command string to apply :type cmdline: str """ - mode = self.current_buffer.typename - self.commandprompthistory.append(cmdline) - cmd = interpret_commandline(cmdline, mode) - if cmd: - self.apply_command(cmd) - else: - self.notify('invalid command') + if cmdline: + mode = self.current_buffer.typename + self.commandprompthistory.append(cmdline) + cmd = interpret_commandline(cmdline, mode) + if cmd: + self.apply_command(cmd) + else: + self.notify('invalid command') def buffer_open(self, b): """ -- cgit v1.2.3 From 39ded8ec057c56018a65fd5ceb27f1a2ea75b9cf Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sat, 10 Sep 2011 20:01:24 +0100 Subject: moved history handling and callback to widget --- alot/widgets.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/alot/widgets.py b/alot/widgets.py index 4fe7439b..a5b08673 100644 --- a/alot/widgets.py +++ b/alot/widgets.py @@ -63,7 +63,7 @@ class ThreadlineWidget(urwid.AttrMap): authors = self.thread.get_authors() or '(None)' maxlength = config.getint('general', 'authors_maxlength') - authorsstring = shorten(authors, maxlength).strip() + authorsstring = shorten(authors, maxlength) self.authors_w = urwid.AttrMap(urwid.Text(authorsstring), 'threadline_authors') cols.append(('fixed', len(authorsstring), self.authors_w)) @@ -167,8 +167,13 @@ class TagWidget(urwid.AttrMap): class CompleteEdit(urwid.Edit): - def __init__(self, completer, edit_text=u'', **kwargs): + def __init__(self, completer, on_exit, edit_text=u'', + history=None, **kwargs): self.completer = completer + self.on_exit = on_exit + self.history = list(history) # we temporarily add stuff here + self.historypos = None + if not isinstance(edit_text, unicode): edit_text = unicode(edit_text, errors='replace') self.start_completion_pos = len(edit_text) @@ -178,7 +183,7 @@ class CompleteEdit(urwid.Edit): def keypress(self, size, key): cmd = command_map[key] # if we tabcomplete - if cmd in ['next selectable', 'prev selectable']: + if cmd in ['next selectable', 'prev selectable'] and self.completer: # if not already in completion mode if not self.completions: self.completions = [(self.edit_text, self.edit_pos)] + \ @@ -199,6 +204,20 @@ class CompleteEdit(urwid.Edit): if self.edit_pos >= len(self.edit_text): self.edit_text += ' ' self.completions = None + elif key in ['up', 'down']: + if self.history: + if self.historypos == None: + self.history.append(self.edit_text) + self.historypos = len(self.history) - 1 + if key == 'cursor up': + self.historypos = (self.historypos + 1) % len(self.history) + else: + self.historypos = (self.historypos - 1) % len(self.history) + self.set_edit_text(self.history[self.historypos]) + elif cmd == 'select': + self.on_exit(self.edit_text) + elif cmd == 'cancel': + self.on_exit(None) else: result = urwid.Edit.keypress(self, size, key) self.completions = None -- cgit v1.2.3 From d9cc7c847c6a50bdd0e51ad50a9dead4805688e6 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sat, 10 Sep 2011 20:53:10 +0100 Subject: use inlineCallback promts throughout command --- alot/command.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/alot/command.py b/alot/command.py index 3111f2cd..407634e6 100644 --- a/alot/command.py +++ b/alot/command.py @@ -34,6 +34,7 @@ from email.message import Message from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart import urwid +from twisted.internet import defer import buffer import settings @@ -354,6 +355,7 @@ class ComposeCommand(Command): for key, value in headers.items(): self.mail[key] = encode_header(key, value) + @defer.inlineCallbacks def apply(self, ui): # TODO: fill with default header (per account) # get From header @@ -366,11 +368,11 @@ class ComposeCommand(Command): a = accounts[0] else: cmpl = AccountCompleter(ui.accountman) - fromaddress = ui.prompt(prefix='From>', completer=cmpl, tab=1) + fromaddress = yield ui.prompt(prefix='From>', completer=cmpl, tab=1) validaddresses = [a.address for a in accounts] + [None] while fromaddress not in validaddresses: # TODO: not cool ui.notify('no account for this address. ( cancels)') - fromaddress = ui.prompt(prefix='From>', completer=cmpl) + fromaddress = yield ui.prompt(prefix='From>', completer=cmpl) if not fromaddress: ui.notify('canceled') return @@ -379,7 +381,7 @@ class ComposeCommand(Command): #get To header if 'To' not in self.mail: - to = ui.prompt(prefix='To>', + to = yield ui.prompt(prefix='To>', completer=ContactsCompleter(ui.accountman)) if to == None: ui.notify('canceled') @@ -387,7 +389,7 @@ class ComposeCommand(Command): self.mail['To'] = encode_header('to', to) if settings.config.getboolean('general', 'ask_subject') and \ not 'Subject' in self.mail: - subject = ui.prompt(prefix='Subject>') + subject = yield ui.prompt(prefix='Subject>') if subject == None: ui.notify('canceled') return @@ -728,12 +730,13 @@ class SaveAttachmentCommand(Command): self.all = all self.path = path + @defer.inlineCallbacks def apply(self, ui): pcomplete = completion.PathCompleter() if self.all: msg = ui.current_buffer.get_selected_message() if not self.path: - self.path = ui.prompt(prefix='save attachments to:', + self.path = yield ui.prompt(prefix='save attachments to:', text=os.path.join('~', ''), completer=pcomplete) if self.path: @@ -752,7 +755,7 @@ class SaveAttachmentCommand(Command): attachment = focus.get_attachment() filename = attachment.get_filename() if not self.path: - self.path = ui.prompt(prefix='save attachment as:', + self.path = yield ui.prompt(prefix='save attachment as:', text=os.path.join('~', filename), completer=pcomplete) if self.path: -- cgit v1.2.3 From 0f7cea59902421e4a539e4256a2f47f0392dbcb1 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 11 Sep 2011 13:11:34 +0100 Subject: fix saving attachments: issue #48 --- alot/command.py | 20 +++++++++++++------- alot/message.py | 14 +++++++++----- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/alot/command.py b/alot/command.py index 407634e6..50a1b4e8 100644 --- a/alot/command.py +++ b/alot/command.py @@ -740,13 +740,16 @@ class SaveAttachmentCommand(Command): text=os.path.join('~', ''), completer=pcomplete) if self.path: - self.path = os.path.expanduser(self.path) - if os.path.isdir(self.path): + if os.path.isdir(os.path.expanduser(self.path)): for a in msg.get_attachments(): dest = a.save(self.path) - ui.notify('saved attachment as: %s' % dest) + name = a.get_filename() + if name: + ui.notify('saved %s as: %s' % (name,dest)) + else: + ui.notify('saved attachment as: %s' % dest) else: - ui.notify('not a directory: %s' % self.path) + ui.notify('not a directory: %s' % self.path, priority='error') else: ui.notify('canceled') else: # save focussed attachment @@ -755,12 +758,15 @@ class SaveAttachmentCommand(Command): attachment = focus.get_attachment() filename = attachment.get_filename() if not self.path: - self.path = yield ui.prompt(prefix='save attachment as:', + self.path = yield ui.prompt(prefix='save attachment (%s) to:' % filename, text=os.path.join('~', filename), completer=pcomplete) if self.path: - attachment.save(os.path.expanduser(self.path)) - ui.notify('saved attachment as: %s' % self.path) + try: + dest = attachment.save(self.path) + ui.notify('saved attachment as: %s' % dest) + except (IOError, OSError), e: # permission/nonexistant dir issues + ui.notify(str(e), priority='error') else: ui.notify('canceled') diff --git a/alot/message.py b/alot/message.py index 5d9c3fd2..3e5b4bcf 100644 --- a/alot/message.py +++ b/alot/message.py @@ -317,14 +317,18 @@ class Attachment: return "%dK" % size_in_kbyte def save(self, path): - # todo: raise exception if path not dir """save the attachment to disk. Uses self.get_filename in case path is a directory""" - if self.get_filename() and os.path.isdir(path): - path = os.path.join(path, self.get_filename()) - FILE = open(path, "w") + filename = self.get_filename() + path = os.path.expanduser(path) + if os.path.isdir(path): + if filename: + basename= os.path.basename(filename) + FILE = open(os.path.join(path,basename), "w") + else: + FILE = tempfile.NamedTemporaryFile(delete=False, dir=path) else: - FILE = tempfile.NamedTemporaryFile(delete=False) + FILE = open(path, "w") # this throws IOErrors for invalid path FILE.write(self.part.get_payload(decode=True)) FILE.close() return FILE.name -- cgit v1.2.3 From 2ddb270d05e63e17fa943a6fca1d7c102a05b7e1 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 11 Sep 2011 13:14:08 +0100 Subject: default key maps for attachment save (s and S) --- alot/settings.py | 2 ++ data/example.full.rc | 2 ++ 2 files changed, 4 insertions(+) diff --git a/alot/settings.py b/alot/settings.py index 18a3cfd5..616ed1d6 100644 --- a/alot/settings.py +++ b/alot/settings.py @@ -251,6 +251,8 @@ DEFAULTS = { 'g': 'groupreply', 'p': 'print', 'r': 'reply', + 's': 'save', + 'S': 'save --all', '|': 'prompt pipeto ', }, 'taglist-maps': { diff --git a/data/example.full.rc b/data/example.full.rc index 124db49c..de456192 100644 --- a/data/example.full.rc +++ b/data/example.full.rc @@ -104,6 +104,8 @@ f = forward g = groupreply p = print r = reply +s = save +S = save --all | = prompt pipeto -- cgit v1.2.3 From cc3bf859e6715626e9e805e25cf4c9ebbd97a814 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 11 Sep 2011 14:09:57 +0100 Subject: documentation --- NEWS | 10 ++++++++++ README.md | 2 +- USAGE | 2 ++ data/example.full.rc | 2 +- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index 5b56780e..3aae9972 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,13 @@ +0.12 + +* smarter in-string-tabcompletion +* added ability to pipe messages/treads to custom shellcommands +* initial searchstring configurable in configfile +* prompt non-blocking (new syntax for prompts!) +* fix attachment saving + +Thanks: Ruben Pollan, Luke Macken + 0.11 This minor release is mostly bug fixes and some small features. diff --git a/README.md b/README.md index fca69be2..ac7dd0f4 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ the `docs` directory contains their sources. Do comment on the code or file issues! I'm curious what you think of it. You can talk to me in #notmuch@Freenode. Be aware that the master branch is used only for releases and hotfixes, -the bleeding edge version sits in branch `development`!. +the bleeding edge version sits in branch `develop`!. If you'd like to contribute, please make sure your patches can be applied to that branch. Current features include: diff --git a/USAGE b/USAGE index 8db4a62f..2439554c 100644 --- a/USAGE +++ b/USAGE @@ -48,12 +48,14 @@ in the config file. These are the default keymaps: E = unfold --all H = toggleheaders P = print --all + S = save --all a = toggletag inbox enter = select f = forward g = groupreply p = print r = reply + s = save | = prompt pipeto Config diff --git a/data/example.full.rc b/data/example.full.rc index de456192..4b2dd264 100644 --- a/data/example.full.rc +++ b/data/example.full.rc @@ -98,6 +98,7 @@ C = fold --all E = unfold --all H = toggleheaders P = print --all +S = save --all a = toggletag inbox enter = select f = forward @@ -105,7 +106,6 @@ g = groupreply p = print r = reply s = save -S = save --all | = prompt pipeto -- cgit v1.2.3 From 971d5ea55715ae899137e69703ee2aed9d281143 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 11 Sep 2011 14:15:36 +0100 Subject: pep8 fixes --- alot/command.py | 28 +++++++++++++++++----------- alot/init.py | 2 +- alot/message.py | 4 ++-- alot/settings.py | 2 +- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/alot/command.py b/alot/command.py index 50a1b4e8..a505517e 100644 --- a/alot/command.py +++ b/alot/command.py @@ -368,11 +368,13 @@ class ComposeCommand(Command): a = accounts[0] else: cmpl = AccountCompleter(ui.accountman) - fromaddress = yield ui.prompt(prefix='From>', completer=cmpl, tab=1) + fromaddress = yield ui.prompt(prefix='From>', completer=cmpl, + tab=1) validaddresses = [a.address for a in accounts] + [None] while fromaddress not in validaddresses: # TODO: not cool ui.notify('no account for this address. ( cancels)') - fromaddress = yield ui.prompt(prefix='From>', completer=cmpl) + fromaddress = yield ui.prompt(prefix='From>', + completer=cmpl) if not fromaddress: ui.notify('canceled') return @@ -657,8 +659,8 @@ class ToggleHeaderCommand(Command): class PipeCommand(Command): def __init__(self, command, whole_thread=False, separately=False, - noop_msg='no command specified', confirm_msg='', done_msg='done', - **kwargs): + noop_msg='no command specified', confirm_msg='', + done_msg='done', **kwargs): Command.__init__(self, **kwargs) self.cmd = command self.whole_thread = whole_thread @@ -745,11 +747,12 @@ class SaveAttachmentCommand(Command): dest = a.save(self.path) name = a.get_filename() if name: - ui.notify('saved %s as: %s' % (name,dest)) + ui.notify('saved %s as: %s' % (name, dest)) else: ui.notify('saved attachment as: %s' % dest) else: - ui.notify('not a directory: %s' % self.path, priority='error') + ui.notify('not a directory: %s' % self.path, + priority='error') else: ui.notify('canceled') else: # save focussed attachment @@ -758,14 +761,16 @@ class SaveAttachmentCommand(Command): attachment = focus.get_attachment() filename = attachment.get_filename() if not self.path: - self.path = yield ui.prompt(prefix='save attachment (%s) to:' % filename, - text=os.path.join('~', filename), - completer=pcomplete) + msg = 'save attachment (%s) to:' % filename + initialtext = os.path.join('~', filename) + self.path = yield ui.prompt(prefix=msg, + completer=pcomplete, + text=initialtext) if self.path: try: dest = attachment.save(self.path) ui.notify('saved attachment as: %s' % dest) - except (IOError, OSError), e: # permission/nonexistant dir issues + except (IOError, OSError), e: ui.notify(str(e), priority='error') else: ui.notify('canceled') @@ -1140,7 +1145,8 @@ def interpret_commandline(cmdline, mode): return commandfactory(cmd, mode=mode, path=filepath) elif cmd == 'print': args = [a.strip() for a in params.split()] - return commandfactory(cmd, mode=mode, whole_thread=('--thread' in args), + return commandfactory(cmd, mode=mode, + whole_thread=('--thread' in args), separately=('--separately' in args)) elif cmd == 'pipeto': return commandfactory(cmd, mode=mode, command=params) diff --git a/alot/init.py b/alot/init.py index 9972e42f..07552b57 100755 --- a/alot/init.py +++ b/alot/init.py @@ -88,7 +88,7 @@ def main(): command_map['esc'] = 'cancel' # get initial searchstring - query = settings.config.get('general','initial_searchstring') + query = settings.config.get('general', 'initial_searchstring') if args.query != '': query = args.query diff --git a/alot/message.py b/alot/message.py index 3e5b4bcf..fc2a7477 100644 --- a/alot/message.py +++ b/alot/message.py @@ -323,8 +323,8 @@ class Attachment: path = os.path.expanduser(path) if os.path.isdir(path): if filename: - basename= os.path.basename(filename) - FILE = open(os.path.join(path,basename), "w") + basename = os.path.basename(filename) + FILE = open(os.path.join(path, basename), "w") else: FILE = tempfile.NamedTemporaryFile(delete=False, dir=path) else: diff --git a/alot/settings.py b/alot/settings.py index 616ed1d6..16784abb 100644 --- a/alot/settings.py +++ b/alot/settings.py @@ -42,7 +42,7 @@ DEFAULTS = { 'bug_on_exit': 'False', 'timestamp_format': '', 'print_cmd': '', - 'initial_searchstring': 'tag:inbox AND NOT tag:killed', + 'initial_searchstring': 'tag:inbox AND NOT tag:killed', }, '16c-theme': { 'bufferlist_focus_bg': 'dark gray', -- cgit v1.2.3 From 26e1cfbe3425787937d3ff7cc875789d94464fb9 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 11 Sep 2011 20:24:25 +0100 Subject: nonblocking ui.choice issue #52 --- alot/ui.py | 73 ++++++++++++++++++++++++++++++++++----------------------- alot/widgets.py | 30 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 29 deletions(-) diff --git a/alot/ui.py b/alot/ui.py index 1ee366f1..5256467f 100644 --- a/alot/ui.py +++ b/alot/ui.py @@ -25,7 +25,7 @@ from settings import config from buffer import BufferlistBuffer from command import commandfactory from command import interpret_commandline -from widgets import CompleteEdit +import widgets from completion import CommandLineCompleter @@ -104,7 +104,7 @@ class UI: #set up widgets leftpart = urwid.Text(prefix, align='left') - editpart = CompleteEdit(completer, on_exit=select_or_cancel, + editpart = widgets.CompleteEdit(completer, on_exit=select_or_cancel, edit_text=text, history=history) for i in range(tab): # hit some tabs @@ -236,42 +236,57 @@ class UI: self.notificationbar = None self.update() - def choice(self, message, choices={'yes': ['y'], 'no': ['n']}): + def choice(self, message, choices={'y': 'yes', 'n': 'no'}, + select=None, cancel=None, msg_position='above'): """prompt user to make a choice :param message: string to display before list of choices :type message: unicode :param choices: dict of possible choices - :type choices: str->list of keys + :type choices: keymap->choice (both str) + :param select: choice to return if enter/return is hit. + Ignored if set to None. + :type select: str + :param cancel: choice to return if escape is hit. + Ignored if set to None. + :type cancel: str + :returns: a `twisted.defer.Deferred` """ - def build_line(msg, prio): - cols = urwid.Columns([urwid.Text(msg)]) - return urwid.AttrMap(cols, 'notify_' + prio) + assert select in choices.values() + [None] + assert cancel in choices.values() + [None] + assert msg_position in ['left', 'above'] - cstrings = ['(%s):%s' % ('/'.join(v), k) for k, v in choices.items()] - line = ', '.join(cstrings) - msgs = [build_line(message + ' ' + line, 'normal')] + d = defer.Deferred() # create return deferred + main = self.mainloop.widget # save main widget - footer = self.mainframe.get_footer() - if not self.notificationbar: - self.notificationbar = urwid.Pile(msgs) - else: - newpile = self.notificationbar.widget_list + msgs - self.notificationbar = urwid.Pile(newpile) - self.update() + def select_or_cancel(text): + self.mainloop.widget = main # restore main screen + d.callback(text) - self.mainloop.draw_screen() - while True: - result = self.mainloop.screen.get_input() - self.logger.info('got: %s ' % result) - if not result: - self.clear_notify(msgs) - self.mainloop.screen.get_input() - return None - for k, v in choices.items(): - if result[0] in v: - self.clear_notify(msgs) - return k + #set up widgets + msgpart = urwid.Text(message) + choicespart = widgets.ChoiceWidget(choices, callback=select_or_cancel, + select=select, cancel=cancel) + + # build widget + if msg_position == 'left': + both = urwid.Columns( + [ + ('fixed', len(message), msgpart), + ('weight', 1, choicespart), + ], dividechars=1) + else: # above + both = urwid.Pile([msgpart, choicespart]) + prompt_widget = urwid.AttrMap(both, 'prompt', 'prompt') + + # put promptwidget as overlay on main widget + overlay = urwid.Overlay(both, main, + ('fixed left', 0), + ('fixed right', 0), + ('fixed bottom', 1), + None) + self.mainloop.widget = overlay + return d # return deferred def notify(self, message, priority='normal', timeout=0, block=False): """notify popup diff --git a/alot/widgets.py b/alot/widgets.py index a5b08673..1645aff5 100644 --- a/alot/widgets.py +++ b/alot/widgets.py @@ -166,6 +166,36 @@ class TagWidget(urwid.AttrMap): self.set_attr_map({None: config.get_tagattr(self.tag)}) +class ChoiceWidget(urwid.Text): + def __init__(self, choices, callback, cancel=None, select=None): + self.choices = choices + self.callback = callback + self.cancel = cancel + self.select = select + + items = [] + for k, v in choices.items(): + if v == select and select != None: + items.append('[%s]:%s' % (k, v)) + else: + items.append('(%s):%s' % (k, v)) + urwid.Text.__init__(self, ' '.join(items)) + + def selectable(self): + return True + + def keypress(self, size, key): + cmd = command_map[key] + if cmd == 'select' and self.select != None: + self.callback(self.select) + elif cmd == 'cancel' and self.cancel != None: + self.callback(self.cancel) + elif key in self.choices: + self.callback(self.choices[key]) + else: + return key + + class CompleteEdit(urwid.Edit): def __init__(self, completer, on_exit, edit_text=u'', history=None, **kwargs): -- cgit v1.2.3 From 461f67bb8b7836b6c465657484114f6b85fdf0ad Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 11 Sep 2011 20:49:16 +0100 Subject: use nonblocking ui.choice --- alot/command.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/alot/command.py b/alot/command.py index a505517e..bf63b8a6 100644 --- a/alot/command.py +++ b/alot/command.py @@ -34,7 +34,7 @@ from email.message import Message from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart import urwid -from twisted.internet import defer +from twisted.internet import reactor, defer import buffer import settings @@ -64,12 +64,13 @@ class Command: class ExitCommand(Command): """shuts the MUA down cleanly""" + @defer.inlineCallbacks def apply(self, ui): if settings.config.getboolean('general', 'bug_on_exit'): - if not ui.choice('realy quit?', choices={'yes': ['y', 'enter'], - 'no': ['n']}) == 'yes': - return - raise urwid.ExitMainLoop() + if (yield ui.choice('realy quit?', select='yes', cancel='no', + msg_position='left')) == 'yes': + reactor.stop() + raise urwid.ExitMainLoop() class OpenThreadCommand(Command): @@ -99,11 +100,12 @@ class SearchCommand(Command): self.query = query Command.__init__(self, **kwargs) + @defer.inlineCallbacks def apply(self, ui): if self.query: if self.query == '*' and ui.current_buffer: s = 'really search for all threads? This takes a while..' - if not ui.choice(s) == 'yes': + if (yield ui.choice(s, select='yes', cancel='no')) == 'no': return open_searches = ui.get_buffers_of_type(buffer.SearchBuffer) to_be_focused = None @@ -441,11 +443,12 @@ class RefineCommand(Command): self.querystring = query Command.__init__(self, **kwargs) + @defer.inlineCallbacks def apply(self, ui): if self.querystring: if self.querystring == '*': s = 'really search for all threads? This takes a while..' - if not ui.choice(s) == 'yes': + if (yield ui.choice(s, select='yes', cancel='no')) == 'no': return sbuffer = ui.current_buffer oldquery = sbuffer.querystring @@ -669,6 +672,7 @@ class PipeCommand(Command): self.confirm_msg = confirm_msg self.done_msg = done_msg + @defer.inlineCallbacks def apply(self, ui): # abort if command unset if not self.cmd: @@ -684,7 +688,8 @@ class PipeCommand(Command): # ask for confirmation if needed if self.confirm_msg: - if not ui.choice(self.confirm_msg) == 'yes': + if (yield ui.choice(self.confirm_msg, select='yes', + cancel='no')) == 'no': return # prepare message sources @@ -925,6 +930,7 @@ class EnvelopeSetCommand(Command): class EnvelopeSendCommand(Command): + @defer.inlineCallbacks def apply(self, ui): envelope = ui.current_buffer mail = envelope.get_email() @@ -944,7 +950,8 @@ class EnvelopeSendCommand(Command): else: ui.notify('could not locate signature: %s' % sig, priority='error') - if not ui.choice('send without signature') == 'yes': + if (yield ui.choice('send without signature', + select='yes', cancel='no')) == 'no': return clearme = ui.notify('sending..', timeout=-1, block=False) -- cgit v1.2.3 From 9492e9bc102dbfbbea0c6031fd8d07d8c77308b9 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 11 Sep 2011 21:28:10 +0100 Subject: fix handling of spaces in attachment filenames --- alot/command.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alot/command.py b/alot/command.py index bf63b8a6..fe66f320 100644 --- a/alot/command.py +++ b/alot/command.py @@ -794,9 +794,9 @@ class OpenAttachmentCommand(Command): if handler: path = self.attachment.save(tempfile.gettempdir()) if '%s' in handler: - cmd = handler % path.replace(' ', '\ ') + cmd = handler % path else: - cmd = '%s %s' % (handler, path.replace(' ', '\ ')) + cmd = '%s %s' % (handler, path) def afterwards(): os.remove(path) -- cgit v1.2.3 From 5e0de3f7e7a4141f4c814c51dc31155ca8602ee2 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 11 Sep 2011 21:30:36 +0100 Subject: fix bug_on_exit --- alot/command.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/alot/command.py b/alot/command.py index fe66f320..3a348af5 100644 --- a/alot/command.py +++ b/alot/command.py @@ -68,9 +68,10 @@ class ExitCommand(Command): def apply(self, ui): if settings.config.getboolean('general', 'bug_on_exit'): if (yield ui.choice('realy quit?', select='yes', cancel='no', - msg_position='left')) == 'yes': - reactor.stop() - raise urwid.ExitMainLoop() + msg_position='left')) == 'no': + return + reactor.stop() + raise urwid.ExitMainLoop() class OpenThreadCommand(Command): -- cgit v1.2.3 From 146b63aaf9e229ba6e828cb9a8434b241e229a79 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Thu, 15 Sep 2011 19:50:00 +0100 Subject: fix error on retag on empty search --- alot/command.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/alot/command.py b/alot/command.py index 3a348af5..229b163e 100644 --- a/alot/command.py +++ b/alot/command.py @@ -405,26 +405,34 @@ class ComposeCommand(Command): # SEARCH class RetagPromptCommand(Command): - """start a commandprompt to retag selected threads' tags""" + """start a commandprompt to retag selected threads' tags + this is needed to fill the prompt with the current tags.. + """ def apply(self, ui): thread = ui.current_buffer.get_selected_thread() + if not thread: + return initial_tagstring = ','.join(thread.get_tags()) ui.commandprompt('retag ' + initial_tagstring) class RetagCommand(Command): """tag selected thread""" - def __init__(self, tagsstring=u'', **kwargs): + def __init__(self, tagsstring=u'', thread=None, **kwargs): self.tagsstring = tagsstring + self.thread = thread Command.__init__(self, **kwargs) def apply(self, ui): - thread = ui.current_buffer.get_selected_thread() - initial_tagstring = ','.join(thread.get_tags()) + if not self.thread: + self.thread = ui.current_buffer.get_selected_thread() + if not self.thread: + return + initial_tagstring = ','.join(self.thread.get_tags()) tags = filter(lambda x: x, self.tagsstring.split(',')) ui.logger.info("got %s:%s" % (self.tagsstring, tags)) try: - thread.set_tags(tags) + self.thread.set_tags(tags) except DatabaseROError, e: ui.notify('index in read-only mode', priority='error') return @@ -683,6 +691,8 @@ class PipeCommand(Command): # get messages to pipe if self.whole_thread: thread = ui.current_buffer.get_selected_thread() + if not thread: + return to_print = thread.get_messages().keys() else: to_print = [ui.current_buffer.get_selected_message()] -- cgit v1.2.3 From 54e6be9e3016d02b4a3e5c6233581944ed406e3d Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Thu, 15 Sep 2011 20:13:51 +0100 Subject: fiz typo with abook_regexp set in config --- alot/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alot/account.py b/alot/account.py index 014ed087..3fd7769c 100644 --- a/alot/account.py +++ b/alot/account.py @@ -190,7 +190,7 @@ class AccountManager: errors='ignore') options.remove('abook_command') if 'abook_regexp' in options: - rgexp = config.get(s, 'abook_regexp') + regexp = config.get(s, 'abook_regexp') options.remove('abook_regexp') else: regexp = None # will use default in constructor -- cgit v1.2.3 From cc120c396b32e5115335ee208e0e7b88458f50a5 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Thu, 15 Sep 2011 23:22:24 +0100 Subject: config option: complete_matching_abook_only in case more than one account has an addressbook: Set this to True to make tabcompletion for recipients during compose only look in the abook of the account matching the sender address --- alot/settings.py | 23 ++++++++++++----------- data/example.full.rc | 6 +++++- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/alot/settings.py b/alot/settings.py index 16784abb..9b5d2489 100644 --- a/alot/settings.py +++ b/alot/settings.py @@ -26,23 +26,24 @@ from ConfigParser import SafeConfigParser DEFAULTS = { 'general': { + 'ask_subject': 'True', + 'authors_maxlength': '30', + 'bug_on_exit': 'False', 'colourmode': '256', + 'complete_matching_abook_only': 'False', + 'display_content_in_threadline': 'False', + 'displayed_headers': 'From,To,Cc,Bcc,Subject', 'editor_cmd': "/usr/bin/vim -f -c 'set filetype=mail' +", 'editor_writes_encoding': 'UTF-8', - 'terminal_cmd': 'x-terminal-emulator -e', - 'spawn_editor': 'False', - 'displayed_headers': 'From,To,Cc,Bcc,Subject', - 'display_content_in_threadline': 'False', - 'authors_maxlength': '30', - 'ask_subject': 'True', - 'notify_timeout': '2', - 'show_statusbar': 'True', 'flush_retry_timeout': '5', 'hooksfile': '~/.alot.py', - 'bug_on_exit': 'False', - 'timestamp_format': '', - 'print_cmd': '', 'initial_searchstring': 'tag:inbox AND NOT tag:killed', + 'notify_timeout': '2', + 'print_cmd': '', + 'show_statusbar': 'True', + 'spawn_editor': 'False', + 'terminal_cmd': 'x-terminal-emulator -e', + 'timestamp_format': '', }, '16c-theme': { 'bufferlist_focus_bg': 'dark gray', diff --git a/data/example.full.rc b/data/example.full.rc index 4b2dd264..4b1ca7f4 100644 --- a/data/example.full.rc +++ b/data/example.full.rc @@ -52,7 +52,11 @@ print_cmd = #initial searchstring when none is given as argument: initial_searchstring = tag:inbox AND NOT tag:killed - + +# in case more than one account has an addressbook: +# Set this to True to make tabcompletion for recipients during compose only +# look in the abook of the account matching the sender address +complete_matching_abook_only = False [global-maps] $ = flush -- cgit v1.2.3 From 51ce77e10f59629986068e2ae62919e030b2f688 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Thu, 15 Sep 2011 23:23:38 +0100 Subject: ContactsCompleter respects given order on accounts --- alot/account.py | 9 +++++++-- alot/command.py | 7 +++++-- alot/completion.py | 11 ++++++----- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/alot/account.py b/alot/account.py index 3fd7769c..73d37377 100644 --- a/alot/account.py +++ b/alot/account.py @@ -242,8 +242,13 @@ class AccountManager: """returns addresses of known accounts including all their aliases""" return self.accountmap.keys() - def get_addressbooks(self): - return [a.abook for a in self.accounts if a.abook] + def get_addressbooks(self, order=[], append_remaining=True): + abooks = [a.abook for a in order] + if append_remaining: + for a in self.accounts: + if a.abook and a.abook not in abooks: + abooks.append(a.abook) + return abooks class AddressBook: diff --git a/alot/command.py b/alot/command.py index 229b163e..cb11154a 100644 --- a/alot/command.py +++ b/alot/command.py @@ -386,8 +386,11 @@ class ComposeCommand(Command): #get To header if 'To' not in self.mail: - to = yield ui.prompt(prefix='To>', - completer=ContactsCompleter(ui.accountman)) + allbooks = settings.config.getboolean('general', + 'complete_matching_abook_only') + abooks = ui.accountman.get_addressbooks(order=[a], + append_remaining=not allbooks) + to = yield ui.prompt(prefix='To>',completer=ContactsCompleter(abooks)) if to == None: ui.notify('canceled') return diff --git a/alot/completion.py b/alot/completion.py index 3cc673aa..f31d0d69 100644 --- a/alot/completion.py +++ b/alot/completion.py @@ -52,8 +52,8 @@ class QueryCompleter(Completer): """completion for a notmuch query string""" def __init__(self, dbman, accountman): self.dbman = dbman - self._contactscompleter = ContactsCompleter(accountman, - addressesonly=True) + abooks = accountman.get_addressbooks() + self._contactscompleter = ContactsCompleter(abooks, addressesonly=True) self._tagscompleter = TagsCompleter(dbman) self.keywords = ['tag', 'from', 'to', 'subject', 'attachment', 'is', 'id', 'thread', 'folder'] @@ -114,8 +114,8 @@ class TagsCompleter(Completer): class ContactsCompleter(Completer): """completes contacts""" - def __init__(self, accountman, addressesonly=False): - self.abooks = accountman.get_addressbooks() + def __init__(self, abooks, addressesonly=False): + self.abooks = abooks self.addressesonly = addressesonly def complete(self, original, pos): @@ -174,7 +174,8 @@ class CommandLineCompleter(Completer): self._commandcompleter = CommandCompleter(dbman, mode) self._querycompleter = QueryCompleter(dbman, accountman) self._tagscompleter = TagsCompleter(dbman) - self._contactscompleter = ContactsCompleter(accountman) + abooks = accountman.get_addressbooks() + self._contactscompleter = ContactsCompleter(abooks, addressesonly=True) self._pathcompleter = PathCompleter() def complete(self, line, pos): -- cgit v1.2.3 From a84d7216dcee626aa4bf85e44b275c27ce22e829 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Thu, 15 Sep 2011 23:26:57 +0100 Subject: updated news --- NEWS | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS b/NEWS index 3aae9972..5cc50c35 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,6 @@ 0.12 +* recipient completion gives priority to abook of sender account * smarter in-string-tabcompletion * added ability to pipe messages/treads to custom shellcommands * initial searchstring configurable in configfile -- cgit v1.2.3 From f17c82bab89abedff9aaf332c0694668c99b9803 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Thu, 15 Sep 2011 23:56:58 +0100 Subject: fix use shlex for OpenAttachment, pep8 this fixes opening of attachments with filenames that include brackets. Note: shlex can't parse unicode! if utf8 attachment filenames happen this might lead to problems --- alot/command.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/alot/command.py b/alot/command.py index cb11154a..a20ddd22 100644 --- a/alot/command.py +++ b/alot/command.py @@ -179,8 +179,9 @@ class ExternalCommand(Command): cmd = '%s %s' % (settings.config.get('general', 'terminal_cmd'), cmd) + cmd = cmd.encode('ascii', errors='ignore') ui.logger.info('calling external command: %s' % cmd) - returncode = subprocess.call(cmd, shell=True) + returncode = subprocess.call(shlex.split(cmd)) if returncode == 0: os.write(write_fd, 'success') @@ -387,10 +388,11 @@ class ComposeCommand(Command): #get To header if 'To' not in self.mail: allbooks = settings.config.getboolean('general', - 'complete_matching_abook_only') + 'complete_matching_abook_only') abooks = ui.accountman.get_addressbooks(order=[a], - append_remaining=not allbooks) - to = yield ui.prompt(prefix='To>',completer=ContactsCompleter(abooks)) + append_remaining=not allbooks) + to = yield ui.prompt(prefix='To>', + completer=ContactsCompleter(abooks)) if to == None: ui.notify('canceled') return -- cgit v1.2.3 From b4b2e7c7affcd5a9fb776da6b9820d170fd8aab8 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sat, 17 Sep 2011 16:18:18 +0100 Subject: added hooks for inline forward and reply quotes define a hook reply_prefix(name,address,datetime) to set the content of the first inline quoted line in reply messages. similarly for forward. --- alot/command.py | 20 ++++++++++++++++---- alot/settings.py | 5 +---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/alot/command.py b/alot/command.py index a20ddd22..57f7d617 100644 --- a/alot/command.py +++ b/alot/command.py @@ -502,8 +502,14 @@ class ReplyCommand(Command): self.message = ui.current_buffer.get_selected_message() mail = self.message.get_email() # set body text - mailcontent = '\nOn %s, %s wrote:\n' % (self.message.get_datestring(), - self.message.get_author()[0]) + name, address = self.message.get_author() + timestamp = self.message.get_date() + qf = settings.hooks.get('reply_prefix') + if qf: + quotestring = qf(name, address, timestamp) + else: + quotestring = 'Quoting %s (%s)\n' % (name, timestamp) + mailcontent = quotestring for line in self.message.accumulate_body().splitlines(): mailcontent += '>' + line + '\n' @@ -604,8 +610,14 @@ class ForwardCommand(Command): Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8') if self.inline: # inline mode # set body text - author = self.message.get_author()[0] - mailcontent = '\nForwarded message from %s:\n' % author + name, address = self.message.get_author() + timestamp = self.message.get_date() + qf = settings.hooks.get('forward_prefix') + if qf: + quotestring = qf(name, address, timestamp) + else: + quotestring = 'Forwarded message from %s (%s):\n' % (name, timestamp) + mailcontent = quotestring for line in self.message.accumulate_body().splitlines(): mailcontent += '>' + line + '\n' diff --git a/alot/settings.py b/alot/settings.py index 9b5d2489..feabedaf 100644 --- a/alot/settings.py +++ b/alot/settings.py @@ -403,10 +403,7 @@ class HookManager: if self.module: if key in self.module.__dict__: return self.module.__dict__[key] - - def f(*args, **kwargs): - msg = 'called undefined hook: %s with arguments' - return f + return None config = AlotConfigParser(DEFAULTS) -- cgit v1.2.3 From 098577ab640e387069b2c2546581ed677fc810a8 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sat, 17 Sep 2011 17:39:53 +0100 Subject: more liberal lookup for accounts --- alot/account.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/alot/account.py b/alot/account.py index 73d37377..74e9beea 100644 --- a/alot/account.py +++ b/alot/account.py @@ -228,8 +228,9 @@ class AccountManager: :rtype: `account.Account` or None """ - if address in self.accountmap: - return self.accountmap[address] + for myad in self.get_addresses(): + if myad in address: + return self.accountmap[myad] else: return None # log info @@ -243,7 +244,11 @@ class AccountManager: return self.accountmap.keys() def get_addressbooks(self, order=[], append_remaining=True): - abooks = [a.abook for a in order] + abooks = [] + for a in order: + if a: + if a.abook: + abooks.append(a.abook) if append_remaining: for a in self.accounts: if a.abook and a.abook not in abooks: -- cgit v1.2.3 From fca437cfda167e62edd9234f73c61ad45ccfb05e Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sat, 17 Sep 2011 17:40:09 +0100 Subject: fix: forwarding --- alot/command.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/alot/command.py b/alot/command.py index 57f7d617..393aafdc 100644 --- a/alot/command.py +++ b/alot/command.py @@ -387,10 +387,15 @@ class ComposeCommand(Command): #get To header if 'To' not in self.mail: - allbooks = settings.config.getboolean('general', + name, addr = email.Utils.parseaddr(unicode(self.mail.get('From'))) + a = ui.accountman.get_account_by_address(addr) + + allbooks = not settings.config.getboolean('general', 'complete_matching_abook_only') + ui.logger.debug(allbooks) abooks = ui.accountman.get_addressbooks(order=[a], - append_remaining=not allbooks) + append_remaining=allbooks) + ui.logger.debug(abooks) to = yield ui.prompt(prefix='To>', completer=ContactsCompleter(abooks)) if to == None: @@ -614,10 +619,10 @@ class ForwardCommand(Command): timestamp = self.message.get_date() qf = settings.hooks.get('forward_prefix') if qf: - quotestring = qf(name, address, timestamp) + quote = qf(name, address, timestamp) else: - quotestring = 'Forwarded message from %s (%s):\n' % (name, timestamp) - mailcontent = quotestring + quote = 'Forwarded message from %s (%s):\n' % (name, timestamp) + mailcontent = quote for line in self.message.accumulate_body().splitlines(): mailcontent += '>' + line + '\n' -- cgit v1.2.3 From 90a04bdfeb91404f4d629f682c55e4b259da20ae Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 18 Sep 2011 12:36:01 +0100 Subject: new hook paramter layout all hooks get called with the following as keywords: ui, dbm, aman, log, config --- alot/command.py | 8 ++++++-- alot/ui.py | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/alot/command.py b/alot/command.py index 393aafdc..3e22ec94 100644 --- a/alot/command.py +++ b/alot/command.py @@ -511,7 +511,9 @@ class ReplyCommand(Command): timestamp = self.message.get_date() qf = settings.hooks.get('reply_prefix') if qf: - quotestring = qf(name, address, timestamp) + quotestring = qf(name, address, timestamp, + ui=ui, dbm=ui.dbman, aman=ui.accountman, + log=ui.logger, config=settings.config) else: quotestring = 'Quoting %s (%s)\n' % (name, timestamp) mailcontent = quotestring @@ -619,7 +621,9 @@ class ForwardCommand(Command): timestamp = self.message.get_date() qf = settings.hooks.get('forward_prefix') if qf: - quote = qf(name, address, timestamp) + quote = qf(name, address, timestamp, + ui=ui, dbm=ui.dbman, aman=ui.accountman, + log=ui.logger, config=settings.config) else: quote = 'Forwarded message from %s (%s):\n' % (name, timestamp) mailcontent = quote diff --git a/alot/ui.py b/alot/ui.py index 5256467f..88759289 100644 --- a/alot/ui.py +++ b/alot/ui.py @@ -377,7 +377,9 @@ class UI: if cmd.prehook: self.logger.debug('calling pre-hook') try: - cmd.prehook(self, self.dbman, self.accountman, config) + cmd.prehook(ui=self, dbm=self.dbman, aman=self.accountman, + log=self.logger, config=config) + except: self.logger.exception('prehook failed') self.logger.debug('apply command: %s' % cmd) @@ -385,6 +387,7 @@ class UI: if cmd.posthook: self.logger.debug('calling post-hook') try: - cmd.posthook(self, self.dbman, self.accountman, config) + cmd.posthook(ui=self, dbm=self.dbman, aman=self.accountman, + log=self.logger, config=config) except: self.logger.exception('posthook failed') -- cgit v1.2.3 From 74758720dbe210b33fcdd085d63f708827d9f077 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 18 Sep 2011 12:55:25 +0100 Subject: hooks docu --- USAGE | 192 ------------------------------------------------------------ USAGE.md | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 192 deletions(-) delete mode 100644 USAGE create mode 100644 USAGE.md diff --git a/USAGE b/USAGE deleted file mode 100644 index 2439554c..00000000 --- a/USAGE +++ /dev/null @@ -1,192 +0,0 @@ -Usage -===== -In all views, arrows, page-up/down, j,k and space can be used to move the focus. -Escape cancels prompts. You can hit ":" at any time and type in commands -to the prompt. Any commandline can be mapped by using the "MODE-maps" sections -in the config file. These are the default keymaps: - - [global-maps] - $ = flush - : = prompt - ; = bufferlist - @ = refresh - I = search tag:inbox AND NOT tag:killed - L = taglist - U = search tag:unread - \ = prompt search - d = bclose - m = compose - o = prompt search - q = exit - shift tab = bprevious - tab = bnext - - [bufferlist-maps] - enter = openfocussed - x = closefocussed - - [search-maps] - & = toggletag killed - O = refineprompt - a = toggletag inbox - enter = openthread - l = retagprompt - | = refineprompt - - [envelope-maps] - a = attach - enter = reedit - s = prompt subject - t = prompt to - y = send - - [taglist-maps] - enter = select - - [thread-maps] - C = fold --all - E = unfold --all - H = toggleheaders - P = print --all - S = save --all - a = toggletag inbox - enter = select - f = forward - g = groupreply - p = print - r = reply - s = save - | = prompt pipeto - -Config ------- -Just like offlineimap or notmuch itself, alot reads a config file in the "INI" syntax: -It consists of some sections whose names are given in square brackets, followed by -key-value pairs that use "=" or ":" as separator, ';' and '#' are comment-prefixes. - -The default location for the config file is `~/.alot.rc`. -You can find a complete example config in `data/example.full.rc`. -Here is a key for the interpreted sections: - - [general] - global settings: set your editor etc - - [account X] - defines the account X: realname, email address, sendmail - - [X-maps] - defines keymaps for mode X. possible modes are: - envelope, search, thread, taglist, bufferlist and global. - global-maps are valid in all modes. - - [tag-translate] - defines a map from tagnames to strings that is used when - displaying tags. utf-8 symbols welcome. - - [Xc-theme] - define colour palette for colour mode. X is in {1, 16, 256}. - -All configs are optional, but if you want to send mails you need to -specify at least one account section. -A sample gmail section looks like this: - - [account gmail] - realname = Patrick Totzke - address = patricktotzke@gmail.com - aliases = patricktotzke@googlemail.com - gpg_key = D7D6C5AA - sender_type = sendmail - sendmail_command = msmtp --account=gmail -t - -I use this for my uni-account: - - [account uoe] - realname = Patrick Totzke - address = ... - aliases = foobar@myuni.uk;f.bar@myuni.uk;f.b100@students.myuni.uk - sender_type = sendmail - sendmail_command = msmtp --account=uoe -t - sent_box = maildir:///home/pazz/mail/uoe/Sent - draft_box = maildir:///home/pazz/mail/uoe/Drafts - signature = ~/my_uni_vcard.vcs - signature_filename = p.totzke.vcs - abook_command = abook --mutt-query - - -Caution: Sending mails is only supported via sendmail for now. If you want -to use a sendmail command different from `sendmail`, specify it as `sendmail_command`. - -`send_mailbox` specifies the mailbox where you want outgoing mails to be stored -after successfully sending them. You can use mbox, maildir, mh, babyl and mmdf -in the protocol part of the url. - -The file specified by `signature` is attached to all outgoing mails from this account, optionally renamed to -`signature_filename`. - -If you specified `abook_command`, it will be used for tab completion in queries (to/from) -and in message composition. The command will be called with your prefix as only argument -and its output is searched for name-email pairs. The regular expression used here -defaults to `(?P.+?@.+?)\s+(?P.+)`, which makes it work nicely with `abook --mutt-query`. -You can tune this using the `abook_regexp` option (beware Commandparsers escaping semantic!). - - -Hooks ------ -Before and after every command execution, alot calls this commands pre/post hook: -Hooks are python callables with arity 4 that live in a module specified by -`hooksfile` in the `[global]` section of your config. Per default this points to `~/.alot.py` -For every command X, the callable 'pre_X' will be called before X and 'post_X' afterwards. - -When a hook gets called, it receives instances of - -1. `alot.ui.UI`, the main user interface object that can prompt etc. -2. `alot.db.DBManager`, the applications database manager -3. `alot.account.AccountManager`, can be used to look up account info -4. `alot.settings.config`, a configparser to access the users config - -An autogenerated API doc for these can be found at http://pazz.github.com/alot/ , -the sphinx sources live in the `docs` folder. -As an example, consider this pre-hook for the exit command, -that logs a personalized goodby message: - -```python -def pre_exit(ui, dbman, accountman, config): - accounts = accountman.get_accounts() - if accounts: - ui.logger.info('goodbye, %s!' % accounts[0].realname) - else: - ui.logger.info('goodbye!') -``` - - -Theming -------- -You can change the colour settings in the section `[Xc-theme]`, where X is the -colour mode you use. This defaults to 256, but 16 and 1 are also possible. -The colourmode can be changed in the globals section or given as a commandline -parameter (-C). -The keys in this section should be self explanatory. In 16c and 256c modes you can define Y_fg and -Y_bg for the foreground and background of each keyword Y. These can define colorcodes and flags -like `underline` or `bold`, comma separated if you want more than one. See urwids doc on Attributes: -http://excess.org/urwid/reference.html#AttrSpec -Urwid privides a neat script that makes choosing colours easy, which can be found here: -http://excess.org/urwid/browser/palette_test.py - -See `data/example.full.rc` for a complete list of widgets that can be themed. -Moreover, keywords that start with "tag_" will be used to display specific tags. For instance, you -can use the following to always display the "todo" tag in white on red, when in 256c-mode. - - [256c-theme] - tag_todo_bg = #d66 - tag_todo_fg = white - -You can translate tag strings before displaying them using the [tag-translate] section. -A key=value statement in this section is interpreted as: -Always display the tag `key` as string `value`. Utf-8 symbols are welcome here. -See e.g. http://panmental.de/symbols/info.htm -I personally display my maildir flags like this: - - [tag-translate] - flagged = ⚑ - unread = ✉ - replied = ⇄ diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 00000000..29017198 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,201 @@ +Usage +===== +In all views, arrows, page-up/down, j,k and space can be used to move the focus. +Escape cancels prompts. You can hit ":" at any time and type in commands +to the prompt. Any commandline can be mapped by using the "MODE-maps" sections +in the config file. These are the default keymaps: + + [global-maps] + $ = flush + : = prompt + ; = bufferlist + @ = refresh + I = search tag:inbox AND NOT tag:killed + L = taglist + U = search tag:unread + \ = prompt search + d = bclose + m = compose + o = prompt search + q = exit + shift tab = bprevious + tab = bnext + + [bufferlist-maps] + enter = openfocussed + x = closefocussed + + [search-maps] + & = toggletag killed + O = refineprompt + a = toggletag inbox + enter = openthread + l = retagprompt + | = refineprompt + + [envelope-maps] + a = attach + enter = reedit + s = prompt subject + t = prompt to + y = send + + [taglist-maps] + enter = select + + [thread-maps] + C = fold --all + E = unfold --all + H = toggleheaders + P = print --all + S = save --all + a = toggletag inbox + enter = select + f = forward + g = groupreply + p = print + r = reply + s = save + | = prompt pipeto + +Config +------ +Just like offlineimap or notmuch itself, alot reads a config file in the "INI" syntax: +It consists of some sections whose names are given in square brackets, followed by +key-value pairs that use "=" or ":" as separator, ';' and '#' are comment-prefixes. + +The default location for the config file is `~/.alot.rc`. +You can find a complete example config in `data/example.full.rc`. +Here is a key for the interpreted sections: + + [general] + global settings: set your editor etc + + [account X] + defines the account X: realname, email address, sendmail + + [X-maps] + defines keymaps for mode X. possible modes are: + envelope, search, thread, taglist, bufferlist and global. + global-maps are valid in all modes. + + [tag-translate] + defines a map from tagnames to strings that is used when + displaying tags. utf-8 symbols welcome. + + [Xc-theme] + define colour palette for colour mode. X is in {1, 16, 256}. + +All configs are optional, but if you want to send mails you need to +specify at least one account section. +A sample gmail section looks like this: + + [account gmail] + realname = Patrick Totzke + address = patricktotzke@gmail.com + aliases = patricktotzke@googlemail.com + gpg_key = D7D6C5AA + sender_type = sendmail + sendmail_command = msmtp --account=gmail -t + +I use this for my uni-account: + + [account uoe] + realname = Patrick Totzke + address = ... + aliases = foobar@myuni.uk;f.bar@myuni.uk;f.b100@students.myuni.uk + sender_type = sendmail + sendmail_command = msmtp --account=uoe -t + sent_box = maildir:///home/pazz/mail/uoe/Sent + draft_box = maildir:///home/pazz/mail/uoe/Drafts + signature = ~/my_uni_vcard.vcs + signature_filename = p.totzke.vcs + abook_command = abook --mutt-query + + +Caution: Sending mails is only supported via sendmail for now. If you want +to use a sendmail command different from `sendmail`, specify it as `sendmail_command`. + +`send_mailbox` specifies the mailbox where you want outgoing mails to be stored +after successfully sending them. You can use mbox, maildir, mh, babyl and mmdf +in the protocol part of the url. + +The file specified by `signature` is attached to all outgoing mails from this account, optionally renamed to +`signature_filename`. + +If you specified `abook_command`, it will be used for tab completion in queries (to/from) +and in message composition. The command will be called with your prefix as only argument +and its output is searched for name-email pairs. The regular expression used here +defaults to `(?P.+?@.+?)\s+(?P.+)`, which makes it work nicely with `abook --mutt-query`. +You can tune this using the `abook_regexp` option (beware Commandparsers escaping semantic!). + + +Hooks +----- +Hooks are python callables that live in a module specified by +`hooksfile` in the `[global]` section of your config. Per default this points to `~/.alot.py`. +For every command X, the callable 'pre_X' will be called before X and 'post_X' afterwards. + +When a hook gets called, it receives instances of + + * ui: `alot.ui.UI`, the main user interface object that can prompt etc. + * dbm: `alot.db.DBManager`, the applications database manager + * aman: `alot.account.AccountManager`, can be used to look up account info + * log: `logging.Logger`, to write to logfile + * config: `alot.settings.config`, a configparser to access the users config + +An autogenerated API doc for these can be found at http://pazz.github.com/alot/ , +the sphinx sources live in the `docs` folder. +As an example, consider this pre-hook for the exit command, +that logs a personalized goodby message: + +```python +def pre_exit(aman=None, log=None, **rest): + accounts = aman.get_accounts() + if accounts: + log.info('goodbye, %s!' % accounts[0].realname) + else: + log.info('goodbye!') +``` + +Apart from command pre and posthooks, the following hooks will be interpreted: + * `reply_prefix(realname, address, timestamp, **kwargs)` + Is used to reformat the first indented line in a reply message. + Should return a string and defaults to 'Quoting %s (%s)\n' % (realname, timestamp) + + * `forward_prefix(realname, address, timestamp, **kwargs)` + Is used to reformat the first indented line in a inline forwarded message. + Returns a string and defaults to 'Forwarded message from %s (%s)\n' % (realname, timestamp) + + +Theming +------- +You can change the colour settings in the section `[Xc-theme]`, where X is the +colour mode you use. This defaults to 256, but 16 and 1 are also possible. +The colourmode can be changed in the globals section or given as a commandline +parameter (-C). +The keys in this section should be self explanatory. In 16c and 256c modes you can define Y_fg and +Y_bg for the foreground and background of each keyword Y. These can define colorcodes and flags +like `underline` or `bold`, comma separated if you want more than one. See urwids doc on Attributes: +http://excess.org/urwid/reference.html#AttrSpec +Urwid privides a neat script that makes choosing colours easy, which can be found here: +http://excess.org/urwid/browser/palette_test.py + +See `data/example.full.rc` for a complete list of widgets that can be themed. +Moreover, keywords that start with "tag_" will be used to display specific tags. For instance, you +can use the following to always display the "todo" tag in white on red, when in 256c-mode. + + [256c-theme] + tag_todo_bg = #d66 + tag_todo_fg = white + +You can translate tag strings before displaying them using the [tag-translate] section. +A key=value statement in this section is interpreted as: +Always display the tag `key` as string `value`. Utf-8 symbols are welcome here. +See e.g. http://panmental.de/symbols/info.htm +I personally display my maildir flags like this: + + [tag-translate] + flagged = ⚑ + unread = ✉ + replied = ⇄ -- cgit v1.2.3 From 2e5a5e35572597b1f23b7f689947cc426c51a841 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 18 Sep 2011 19:06:18 +0100 Subject: pre/post edit translate hooks (issue #55) --- USAGE.md | 7 +++++++ alot/command.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/USAGE.md b/USAGE.md index 29017198..ef277a28 100644 --- a/USAGE.md +++ b/USAGE.md @@ -166,6 +166,13 @@ Apart from command pre and posthooks, the following hooks will be interpreted: * `forward_prefix(realname, address, timestamp, **kwargs)` Is used to reformat the first indented line in a inline forwarded message. Returns a string and defaults to 'Forwarded message from %s (%s)\n' % (realname, timestamp) + * `pre_edit_translate(bodytext, **kwargs)` + can be used to manipulate a messages bodytext before the editor is called. + Receives and returns a string. + * `post_edit_translate(bodytext, **kwargs)` + can be used to manipulate a messages bodytext after the editor is called + Receives and returns a string. + Theming diff --git a/alot/command.py b/alot/command.py index 3e22ec94..c0bdf44e 100644 --- a/alot/command.py +++ b/alot/command.py @@ -890,6 +890,13 @@ class EnvelopeEditCommand(Command): editor_input = f.read().decode(enc) headertext, bodytext = editor_input.split('\n\n', 1) + # call post-edit translate hook + translate = settings.hooks.get('post_edit_translate') + if translate: + bodytext = translate(bodytext, ui=ui, dbm=ui.dbman, + aman=ui.accountman, log=ui.logger, + config=settings.config) + # go through multiline, utf-8 encoded headers key = value = None for line in headertext.splitlines(): @@ -936,6 +943,13 @@ class EnvelopeEditCommand(Command): else: bodytext = decode_to_unicode(self.mail) + # call pre-edit translate hook + translate = settings.hooks.get('pre_edit_translate') + if translate: + bodytext = translate(bodytext, ui=ui, dbm=ui.dbman, + aman=ui.accountman, log=ui.logger, + config=settings.config) + #write stuff to tempfile tf = tempfile.NamedTemporaryFile(delete=False) content = '%s\n\n%s' % (headertext, -- cgit v1.2.3 From 05dcb7a9d018b192a950a292af8064c5436ee90c Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 18 Sep 2011 21:51:44 +0100 Subject: fix (semi)colons in example.full.rc --- alot/init.py | 2 +- data/example.full.rc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/alot/init.py b/alot/init.py index 07552b57..8d95fa72 100755 --- a/alot/init.py +++ b/alot/init.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python """ This file is part of alot, a terminal UI to notmuch mail (notmuchmail.org). Copyright (C) 2011 Patrick Totzke diff --git a/data/example.full.rc b/data/example.full.rc index 4b1ca7f4..aaef8d24 100644 --- a/data/example.full.rc +++ b/data/example.full.rc @@ -60,8 +60,8 @@ complete_matching_abook_only = False [global-maps] $ = flush -: = prompt -; = bufferlist +":" = prompt +";" = bufferlist @ = refresh I = search tag:inbox AND NOT tag:killed L = taglist -- cgit v1.2.3 From f60515cfac2cb623f7c9698a2ce10030fbb41637 Mon Sep 17 00:00:00 2001 From: Justus Winter <4winter@informatik.uni-hamburg.de> Date: Thu, 22 Sep 2011 10:13:06 +0200 Subject: Use new-style classes See http://www.python.org/download/releases/2.3/mro/ for all the gory details. --- alot/account.py | 6 +++--- alot/buffer.py | 2 +- alot/command.py | 2 +- alot/completion.py | 2 +- alot/db.py | 4 ++-- alot/message.py | 4 ++-- alot/settings.py | 2 +- alot/ui.py | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/alot/account.py b/alot/account.py index 74e9beea..91ef5fff 100644 --- a/alot/account.py +++ b/alot/account.py @@ -32,7 +32,7 @@ from helper import cmd_output import helper -class Account: +class Account(object): """ Datastructure that represents an email account. It manages this account's settings, can send and store mails to @@ -159,7 +159,7 @@ class SendmailAccount(Account): return None -class AccountManager: +class AccountManager(object): """Easy access to all known accounts""" allowed = ['realname', 'address', @@ -256,7 +256,7 @@ class AccountManager: return abooks -class AddressBook: +class AddressBook(object): def get_contacts(self): return [] diff --git a/alot/buffer.py b/alot/buffer.py index 1ea7766b..77eef99e 100644 --- a/alot/buffer.py +++ b/alot/buffer.py @@ -27,7 +27,7 @@ from walker import IteratorWalker from message import decode_header -class Buffer: +class Buffer(object): def __init__(self, ui, widget, name): self.ui = ui self.typename = name diff --git a/alot/command.py b/alot/command.py index c0bdf44e..d719ca81 100644 --- a/alot/command.py +++ b/alot/command.py @@ -50,7 +50,7 @@ from message import decode_header from message import encode_header -class Command: +class Command(object): """base class for commands""" def __init__(self, prehook=None, posthook=None): self.prehook = prehook diff --git a/alot/completion.py b/alot/completion.py index f31d0d69..38d1eade 100644 --- a/alot/completion.py +++ b/alot/completion.py @@ -25,7 +25,7 @@ import logging import command -class Completer: +class Completer(object): def complete(self, original, pos): """returns a list of completions and cursor positions for the string original from position pos on. diff --git a/alot/db.py b/alot/db.py index 5dca9b88..b96d3841 100644 --- a/alot/db.py +++ b/alot/db.py @@ -38,7 +38,7 @@ class DatabaseLockedError(DatabaseError): pass -class DBManager: +class DBManager(object): """ keeps track of your index parameters, can create notmuch.Query objects from its Database on demand and implements a bunch of @@ -174,7 +174,7 @@ class DBManager: return db.create_query(querystring) -class Thread: +class Thread(object): def __init__(self, dbman, thread): """ :param dbman: db manager that is used for further lookups diff --git a/alot/message.py b/alot/message.py index fc2a7477..e8cf539c 100644 --- a/alot/message.py +++ b/alot/message.py @@ -29,7 +29,7 @@ from settings import get_mime_handler from settings import config -class Message: +class Message(object): def __init__(self, dbman, msg, thread=None): """ :param dbman: db manager that is used for further lookups @@ -279,7 +279,7 @@ def encode_header(key, value): return value -class Attachment: +class Attachment(object): """represents a single mail attachment""" def __init__(self, emailpart): diff --git a/alot/settings.py b/alot/settings.py index feabedaf..1aa7b682 100644 --- a/alot/settings.py +++ b/alot/settings.py @@ -388,7 +388,7 @@ class AlotConfigParser(DefaultsConfigParser): return cmdline -class HookManager: +class HookManager(object): def setup(self, hooksfile): hf = os.path.expanduser(hooksfile) if os.path.isfile(hf): diff --git a/alot/ui.py b/alot/ui.py index 88759289..2e58dc86 100644 --- a/alot/ui.py +++ b/alot/ui.py @@ -45,7 +45,7 @@ class MainWidget(urwid.Frame): urwid.Frame.keypress(self, size, key) -class UI: +class UI(object): buffers = [] current_buffer = None -- cgit v1.2.3 From 4c33283363606079f2cd27a376d2fd012b157f76 Mon Sep 17 00:00:00 2001 From: Justus Winter <4winter@informatik.uni-hamburg.de> Date: Thu, 22 Sep 2011 11:38:03 +0200 Subject: Add shell_quote to helper shell_quote securely quotes strings being used as command line arguments. --- alot/helper.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/alot/helper.py b/alot/helper.py index d20ba844..d7916123 100644 --- a/alot/helper.py +++ b/alot/helper.py @@ -113,3 +113,12 @@ def attach(path, mail, filename=None): part.add_header('Content-Disposition', 'attachment', filename=filename) mail.attach(part) + +def shell_quote(text): + r''' + >>> print(shell_quote("hello")) + 'hello' + >>> print(shell_quote("hello'there")) + 'hello'"'"'there' + ''' + return "'%s'" % text.replace("'", """'"'"'""") -- cgit v1.2.3 From 5add434c57d8e41582eb8f9804c3716ef5ba41e8 Mon Sep 17 00:00:00 2001 From: Justus Winter <4winter@informatik.uni-hamburg.de> Date: Thu, 22 Sep 2011 12:52:28 +0200 Subject: Read defaults from default config file Read the configuration defaults from a file instead of hardcoding them in settings.py. This also simplifies the option lookup. --- alot/defaults/alot.rc | 289 ++++++++++++++++++++++++++++++++++++++++++++++ alot/defaults/notmuch.rc | 3 + alot/settings.py | 295 ++--------------------------------------------- data/example.full.rc | 288 --------------------------------------------- setup.py | 1 + 5 files changed, 303 insertions(+), 573 deletions(-) create mode 100644 alot/defaults/alot.rc create mode 100644 alot/defaults/notmuch.rc delete mode 100644 data/example.full.rc diff --git a/alot/defaults/alot.rc b/alot/defaults/alot.rc new file mode 100644 index 00000000..fbe2b318 --- /dev/null +++ b/alot/defaults/alot.rc @@ -0,0 +1,289 @@ +[general] + +# ask for subject when compose +ask_subject = True + +# max length of authors line in thread widgets +authors_maxlength = 30 + +# confirm exit +bug_on_exit = False + +# number of colours your terminal supports +colourmode = 256 + +# fill threadline with message content +display_content_in_threadline = False + +# headers that get displayed by default +displayed_headers = From,To,Cc,Bcc,Subject + +# editor command +editor_cmd = /usr/bin/vim -f -c 'set filetype=mail' + +editor_writes_encoding' = UTF-8 + +# timeout in secs after a failed attempt to flush is repeated +flush_retry_timeout = 5 + +# where to look up hooks +hooksfile = ~/.alot.py + +# time in secs to display status messages +notify_timeout = 2 + +# display statusline? +show_statusbar = True + +spawn_editor = False +# set terminal for asynchronous editing +terminal_cmd = x-terminal-emulator -e + +# strftime format for timestamps. Note: you must escape % here: +# use '%%' instead of '%'. otherwise see +# http://docs.python.org/library/datetime.html#strftime-strptime-behavior +timestamp_format = '' + +# how to print messages: +# this specifies a shellcommand used pro printing. +# threads/messages are piped to this as plaintext. +# muttprint/a2ps works nicely +print_cmd = + +#initial searchstring when none is given as argument: +initial_searchstring = tag:inbox AND NOT tag:killed + +# in case more than one account has an addressbook: +# Set this to True to make tabcompletion for recipients during compose only +# look in the abook of the account matching the sender address +complete_matching_abook_only = False + +[global-maps] +@ = refresh +I = search tag:inbox AND NOT tag:killed +L = taglist +shift tab = bprevious +U = search tag:unread +tab = bnext +\ = 'prompt search ' +d = bclose +$ = flush +m = compose +o = 'prompt search ' +q = exit +';' = bufferlist +':' = prompt + +[bufferlist-maps] +x = closefocussed +enter = openfocussed + +[search-maps] +a = toggletag inbox +& = toggletag killed +l = retagprompt +O = refineprompt +enter = openthread +| = refineprompt + +[envelope-maps] +a = prompt attach ~/ +y = send +s = 'prompt subject ' +t = 'prompt to ' +enter = reedit + +[taglist-maps] +enter = select + +[thread-maps] +C = fold --all +E = unfold --all +H = toggleheaders +P = print --thread +S = save --all +a = toggletag inbox +g = groupreply +f = forward +p = print +s = save +r = reply +enter = select +| = 'prompt pipeto ' + +[command-aliases] +quit = exit +bn = bnext +clo = close +bp = bprevious +ls = bufferlist + +[16c-theme] +bufferlist_focus_fg = white +threadline_mailcount_focus_bg = dark cyan +bufferlist_focus_bg = dark gray +tag_bg = black +message_attachment_fg = light gray +threadline_authors_focus_fg = black,bold +threadline_bg = default +notify_error_fg = white +bufferlist_results_even_bg = black +threadline_subject_fg = light gray +message_header_value_bg = dark gray +messagesummary_even_fg = white +threadline_authors_focus_bg = dark cyan +footer_fg = light green +bufferlist_results_odd_fg = light gray +threadline_date_fg = light gray +bufferlist_results_even_fg = light gray +message_header_value_fg = light gray +threadline_subject_bg = default +messagesummary_odd_fg = white +message_header_key_fg = white +threadline_tags_focus_bg = dark cyan +tag_focus_fg = white +bufferlist_results_odd_bg = black +threadline_fg = default +prompt_fg = light gray +header_bg = dark blue +threadline_subject_focus_bg = dark cyan +message_body_fg = light gray +message_header_key_bg = dark gray +threadline_date_bg = default +tag_focus_bg = dark cyan +messagesummary_odd_bg = dark blue +threadline_subject_focus_fg = black +threadline_date_focus_fg = black +messagesummary_even_bg = light blue +message_header_fg = white +threadline_focus_fg = white +messagesummary_focus_bg = dark cyan +threadline_date_focus_bg = dark cyan +threadline_content_bg = default +threadline_tags_bg = default +threadline_mailcount_bg = default +messagesummary_focus_fg = white +message_body_bg = default +threadline_mailcount_focus_fg = black +prompt_bg = black +header_fg = white +threadline_content_fg = dark gray +threadline_focus_bg = dark cyan +threadline_mailcount_fg = light gray +threadline_content_focus_bg = dark cyan +tag_fg = brown +notify_normal_fg = default +message_attachment_focussed_bg = light green +threadline_tags_fg = brown +notify_error_bg = dark red +threadline_content_focus_fg = dark gray +footer_bg = dark blue +threadline_authors_bg = default +threadline_tags_focus_fg = yellow,bold +message_attachment_bg = dark gray +notify_normal_bg = default +message_attachment_focussed_fg = light gray +threadline_authors_fg = dark green +message_header_bg = dark gray + +[256c-theme] +bufferlist_focus_fg = #ffa +threadline_mailcount_focus_bg = g58 +bufferlist_focus_bg = g38 +tag_bg = default +message_attachment_fg = light gray +threadline_authors_focus_fg = #8f6 +threadline_bg = default +notify_error_fg = white +bufferlist_results_even_bg = g3 +threadline_subject_fg = g58 +message_header_value_bg = dark gray +messagesummary_even_fg = white +threadline_authors_focus_bg = g58 +footer_fg = white +bufferlist_results_odd_fg = default +threadline_date_fg = g58 +bufferlist_results_even_fg = default +message_header_value_fg = light gray +threadline_subject_bg = default +messagesummary_odd_fg = white +message_header_key_fg = white +threadline_tags_focus_bg = g58 +tag_focus_fg = #ffa +bufferlist_results_odd_bg = default +threadline_fg = default +prompt_fg = light gray +header_bg = dark blue +threadline_subject_focus_bg = g58 +message_body_fg = light gray +message_header_key_bg = dark gray +threadline_date_bg = default +tag_focus_bg = g58 +messagesummary_odd_bg = #006 +threadline_subject_focus_fg = g89 +threadline_date_focus_fg = g89 +messagesummary_even_bg = #068 +message_header_fg = white +threadline_focus_fg = white +messagesummary_focus_bg = g58 +threadline_date_focus_bg = g58 +threadline_content_bg = default +threadline_tags_bg = default +threadline_mailcount_bg = default +messagesummary_focus_fg = #ff8 +message_body_bg = default +threadline_mailcount_focus_fg = g89 +prompt_bg = default +header_fg = white +threadline_content_fg = #866 +threadline_focus_bg = g58 +threadline_mailcount_fg = light gray +threadline_content_focus_bg = g58 +tag_fg = brown +notify_normal_fg = default +message_attachment_focussed_bg = light green +threadline_tags_fg = #a86 +notify_error_bg = dark red +threadline_content_focus_fg = #866 +footer_bg = #006 +threadline_authors_bg = default +threadline_tags_focus_fg = #ff8 +message_attachment_bg = dark gray +notify_normal_bg = default +message_attachment_focussed_fg = light gray +threadline_authors_fg = #6d6 +message_header_bg = dark gray + +[1c-theme] +message_body = default +message_header = default +prompt = +message_attachment = default +notify_normal = default +messagesummary_odd = +threadline_content = default +threadline_subject_focus = standout +bufferlist_results_odd = default +messagesummary_focus = standout +message_attachment_focussed = underline +tag_focus = standout +header = standout +tag = default +threadline_content_focus = standout +message_header_value = default +threadline_mailcount_focus = standout +threadline_date_focus = standout +message_header_key = default +threadline_subject = default +threadline_authors = default,underline +bufferlist_results_even = default +threadline_authors_focus = standout +footer = standout +notify_error = standout +messagesummary_even = +threadline_tags_focus = standout +threadline = default +threadline_date = default +threadline_mailcount = default +threadline_focus = standout +threadline_tags = bold +bufferlist_focus = standout diff --git a/alot/defaults/notmuch.rc b/alot/defaults/notmuch.rc new file mode 100644 index 00000000..f03fcda1 --- /dev/null +++ b/alot/defaults/notmuch.rc @@ -0,0 +1,3 @@ +[maildir] +synchronize_flags = False + diff --git a/alot/settings.py b/alot/settings.py index 1aa7b682..39783ba2 100644 --- a/alot/settings.py +++ b/alot/settings.py @@ -23,301 +23,24 @@ import codecs from ConfigParser import SafeConfigParser - -DEFAULTS = { - 'general': { - 'ask_subject': 'True', - 'authors_maxlength': '30', - 'bug_on_exit': 'False', - 'colourmode': '256', - 'complete_matching_abook_only': 'False', - 'display_content_in_threadline': 'False', - 'displayed_headers': 'From,To,Cc,Bcc,Subject', - 'editor_cmd': "/usr/bin/vim -f -c 'set filetype=mail' +", - 'editor_writes_encoding': 'UTF-8', - 'flush_retry_timeout': '5', - 'hooksfile': '~/.alot.py', - 'initial_searchstring': 'tag:inbox AND NOT tag:killed', - 'notify_timeout': '2', - 'print_cmd': '', - 'show_statusbar': 'True', - 'spawn_editor': 'False', - 'terminal_cmd': 'x-terminal-emulator -e', - 'timestamp_format': '', - }, - '16c-theme': { - 'bufferlist_focus_bg': 'dark gray', - 'bufferlist_focus_fg': 'white', - 'bufferlist_results_even_bg': 'black', - 'bufferlist_results_even_fg': 'light gray', - 'bufferlist_results_odd_bg': 'black', - 'bufferlist_results_odd_fg': 'light gray', - 'footer_bg': 'dark blue', - 'footer_fg': 'light green', - 'header_bg': 'dark blue', - 'header_fg': 'white', - 'message_attachment_bg': 'dark gray', - 'message_attachment_fg': 'light gray', - 'message_attachment_focussed_bg': 'light green', - 'message_attachment_focussed_fg': 'light gray', - 'message_body_bg': 'default', - 'message_body_fg': 'light gray', - 'message_header_bg': 'dark gray', - 'message_header_fg': 'white', - 'message_header_key_bg': 'dark gray', - 'message_header_key_fg': 'white', - 'message_header_value_bg': 'dark gray', - 'message_header_value_fg': 'light gray', - 'messagesummary_even_bg': 'light blue', - 'messagesummary_even_fg': 'white', - 'messagesummary_focus_bg': 'dark cyan', - 'messagesummary_focus_fg': 'white', - 'messagesummary_odd_bg': 'dark blue', - 'messagesummary_odd_fg': 'white', - 'notify_error_bg': 'dark red', - 'notify_error_fg': 'white', - 'notify_normal_bg': 'default', - 'notify_normal_fg': 'default', - 'prompt_bg': 'black', - 'prompt_fg': 'light gray', - 'tag_focus_bg': 'dark cyan', - 'tag_focus_fg': 'white', - 'tag_bg': 'black', - 'tag_fg': 'brown', - 'threadline_authors_bg': 'default', - 'threadline_authors_fg': 'dark green', - 'threadline_authors_focus_bg': 'dark cyan', - 'threadline_authors_focus_fg': 'black,bold', - 'threadline_bg': 'default', - 'threadline_content_bg': 'default', - 'threadline_content_fg': 'dark gray', - 'threadline_content_focus_bg': 'dark cyan', - 'threadline_content_focus_fg': 'dark gray', - 'threadline_date_bg': 'default', - 'threadline_date_fg': 'light gray', - 'threadline_date_focus_bg': 'dark cyan', - 'threadline_date_focus_fg': 'black', - 'threadline_fg': 'default', - 'threadline_focus_bg': 'dark cyan', - 'threadline_focus_fg': 'white', - 'threadline_mailcount_bg': 'default', - 'threadline_mailcount_fg': 'light gray', - 'threadline_mailcount_focus_bg': 'dark cyan', - 'threadline_mailcount_focus_fg': 'black', - 'threadline_subject_bg': 'default', - 'threadline_subject_fg': 'light gray', - 'threadline_subject_focus_bg': 'dark cyan', - 'threadline_subject_focus_fg': 'black', - 'threadline_tags_bg': 'default', - 'threadline_tags_fg': 'brown', - 'threadline_tags_focus_bg': 'dark cyan', - 'threadline_tags_focus_fg': 'yellow,bold', - }, - '1c-theme': { - 'bufferlist_focus': 'standout', - 'bufferlist_results_even': 'default', - 'bufferlist_results_odd': 'default', - 'footer': 'standout', - 'header': 'standout', - 'message_attachment': 'default', - 'message_attachment_focussed': 'underline', - 'message_body': 'default', - 'message_header': 'default', - 'message_header_key': 'default', - 'message_header_value': 'default', - 'messagesummary_even': '', - 'messagesummary_focus': 'standout', - 'messagesummary_odd': '', - 'notify_error': 'standout', - 'notify_normal': 'default', - 'prompt': '', - 'tag_focus': 'standout', - 'tag': 'default', - 'threadline': 'default', - 'threadline_authors': 'default,underline', - 'threadline_authors_focus': 'standout', - 'threadline_content': 'default', - 'threadline_content_focus': 'standout', - 'threadline_date': 'default', - 'threadline_date_focus': 'standout', - 'threadline_focus': 'standout', - 'threadline_mailcount': 'default', - 'threadline_mailcount_focus': 'standout', - 'threadline_subject': 'default', - 'threadline_subject_focus': 'standout', - 'threadline_tags': 'bold', - 'threadline_tags_focus': 'standout', - }, - '256c-theme': { - 'bufferlist_focus_bg': 'g38', - 'bufferlist_focus_fg': '#ffa', - 'bufferlist_results_even_bg': 'g3', - 'bufferlist_results_even_fg': 'default', - 'bufferlist_results_odd_bg': 'default', - 'bufferlist_results_odd_fg': 'default', - 'footer_bg': '#006', - 'footer_fg': 'white', - 'header_bg': 'dark blue', - 'header_fg': 'white', - 'message_attachment_bg': 'dark gray', - 'message_attachment_fg': 'light gray', - 'message_attachment_focussed_bg': 'light green', - 'message_attachment_focussed_fg': 'light gray', - 'message_body_bg': 'default', - 'message_body_fg': 'light gray', - 'message_header_bg': 'dark gray', - 'message_header_fg': 'white', - 'message_header_key_bg': 'dark gray', - 'message_header_key_fg': 'white', - 'message_header_value_bg': 'dark gray', - 'message_header_value_fg': 'light gray', - 'messagesummary_even_bg': '#068', - 'messagesummary_even_fg': 'white', - 'messagesummary_focus_bg': 'g58', - 'messagesummary_focus_fg': '#ff8', - 'messagesummary_odd_bg': '#006', - 'messagesummary_odd_fg': 'white', - 'notify_error_bg': 'dark red', - 'notify_error_fg': 'white', - 'notify_normal_bg': 'default', - 'notify_normal_fg': 'default', - 'prompt_bg': 'default', - 'prompt_fg': 'light gray', - 'tag_focus_bg': 'g58', - 'tag_focus_fg': '#ffa', - 'tag_bg': 'default', - 'tag_fg': 'brown', - 'threadline_authors_bg': 'default', - 'threadline_authors_fg': '#6d6', - 'threadline_authors_focus_bg': 'g58', - 'threadline_authors_focus_fg': '#8f6', - 'threadline_bg': 'default', - 'threadline_content_bg': 'default', - 'threadline_content_fg': '#866', - 'threadline_content_focus_bg': 'g58', - 'threadline_content_focus_fg': '#866', - 'threadline_date_bg': 'default', - 'threadline_date_fg': 'g58', - 'threadline_date_focus_bg': 'g58', - 'threadline_date_focus_fg': 'g89', - 'threadline_fg': 'default', - 'threadline_focus_bg': 'g58', - 'threadline_focus_fg': 'white', - 'threadline_mailcount_bg': 'default', - 'threadline_mailcount_fg': 'light gray', - 'threadline_mailcount_focus_bg': 'g58', - 'threadline_mailcount_focus_fg': 'g89', - 'threadline_subject_bg': 'default', - 'threadline_subject_fg': 'g58', - 'threadline_subject_focus_bg': 'g58', - 'threadline_subject_focus_fg': 'g89', - 'threadline_tags_bg': 'default', - 'threadline_tags_fg': '#a86', - 'threadline_tags_focus_bg': 'g58', - 'threadline_tags_focus_fg': '#ff8', - }, - 'global-maps': { - '$': 'flush', - ':': 'prompt', - ';': 'bufferlist', - '@': 'refresh', - '@': 'refresh', - 'I': 'search tag:inbox AND NOT tag:killed', - 'L': 'taglist', - 'U': 'search tag:unread', - '\\': 'prompt search ', - 'm': 'compose', - 'o': 'prompt search ', - 'q': 'exit', - 'shift tab': 'bprevious', - 'tab': 'bnext', - 'd': 'bclose', - }, - 'search-maps': { - '&': 'toggletag killed', - 'O': 'refineprompt', - 'a': 'toggletag inbox', - 'enter': 'openthread', - 'l': 'retagprompt', - '|': 'refineprompt', - }, - 'thread-maps': { - 'C': 'fold --all', - 'E': 'unfold --all', - 'H': 'toggleheaders', - 'P': 'print --thread', - 'a': 'toggletag inbox', - 'enter': 'select', - 'f': 'forward', - 'g': 'groupreply', - 'p': 'print', - 'r': 'reply', - 's': 'save', - 'S': 'save --all', - '|': 'prompt pipeto ', - }, - 'taglist-maps': { - 'enter': 'select', - }, - 'envelope-maps': { - 'a': 'prompt attach ~/', - 'y': 'send', - 'enter': 'reedit', - 't': 'prompt to ', - 's': 'prompt subject ', - }, - 'bufferlist-maps': { - 'x': 'closefocussed', - 'enter': 'openfocussed', - }, - 'command-aliases': { - 'clo': 'close', - 'bn': 'bnext', - 'bp': 'bprevious', - 'ls': 'bufferlist', - 'quit': 'exit', - } -} - -NOTMUCH_DEFAULTS = { - 'maildir': { - 'synchronize_flags': 'False', - }, -} - - -class DefaultsConfigParser(SafeConfigParser): - def __init__(self, defaults): - self.defaults = defaults +class FallbackConfigParser(SafeConfigParser): + def __init__(self): SafeConfigParser.__init__(self) self.optionxform = lambda x: x - for sec in defaults.keys(): - self.add_section(sec) def get(self, section, option, fallback=None, *args, **kwargs): if SafeConfigParser.has_option(self, section, option): return SafeConfigParser.get(self, section, option, *args, **kwargs) - elif section in self.defaults: - if option in self.defaults[section]: - return self.defaults[section][option] return fallback - def has_option(self, section, option, *args, **kwargs): - if SafeConfigParser.has_option(self, section, option): - return True - elif section in self.defaults: - if option in self.defaults[section]: - return True - return False - def getstringlist(self, section, option, **kwargs): value = self.get(section, option, **kwargs) return [s.strip() for s in value.split(',')] -class AlotConfigParser(DefaultsConfigParser): - def __init__(self, defaults): - DefaultsConfigParser.__init__(self, defaults) +class AlotConfigParser(FallbackConfigParser): + def __init__(self): + FallbackConfigParser.__init__(self) self.hooks = None def read(self, file): @@ -336,7 +59,7 @@ class AlotConfigParser(DefaultsConfigParser): def get_palette(self): mode = self.getint('general', 'colourmode') ms = "%dc-theme" % mode - names = self.options(ms) + DEFAULTS[ms].keys() + names = self.options(ms) if mode > 2: names = set([s[:-3] for s in names]) p = list() @@ -406,8 +129,10 @@ class HookManager(object): return None -config = AlotConfigParser(DEFAULTS) -notmuchconfig = DefaultsConfigParser(NOTMUCH_DEFAULTS) +config = AlotConfigParser() +config.read(os.path.join(os.path.dirname(__file__), 'defaults', 'alot.rc')) +notmuchconfig = FallbackConfigParser() +notmuchconfig.read(os.path.join(os.path.dirname(__file__), 'defaults', 'notmuch.rc')) hooks = HookManager() mailcaps = mailcap.getcaps() diff --git a/data/example.full.rc b/data/example.full.rc deleted file mode 100644 index aaef8d24..00000000 --- a/data/example.full.rc +++ /dev/null @@ -1,288 +0,0 @@ -[general] - -# ask for subject when compose -ask_subject = True - -# max length of authors line in thread widgets -authors_maxlength = 30 - -# confirm exit -bug_on_exit = False - -# number of colours your terminal supports -colourmode = 256 - -# fill threadline with message content -display_content_in_threadline = False - -# headers that get displayed by default -displayed_headers = From,To,Cc,Bcc,Subject - - -# editor command -editor_cmd = /usr/bin/vim -f -c 'set filetype=mail' + -editor_writes_encoding' = UTF-8 - -# timeout in secs after a failed attempt to flush is repeated -flush_retry_timeout = 5 - -# where to look up hooks -hooksfile = ~/.alot.py - -# time in secs to display status messages -notify_timeout = 2 - -# display statusline? -show_statusbar = True - -spawn_editor = False -# set terminal for asynchronous editing -terminal_cmd = x-terminal-emulator -e - -# strftime format for timestamps. Note: you must escape % here: -# use '%%' instead of '%'. otherwise see -# http://docs.python.org/library/datetime.html#strftime-strptime-behavior -timestamp_format = '' - -# how to print messages: -# this specifies a shellcommand used pro printing. -# threads/messages are piped to this as plaintext. -# muttprint/a2ps works nicely -print_cmd = - -#initial searchstring when none is given as argument: -initial_searchstring = tag:inbox AND NOT tag:killed - -# in case more than one account has an addressbook: -# Set this to True to make tabcompletion for recipients during compose only -# look in the abook of the account matching the sender address -complete_matching_abook_only = False - -[global-maps] -$ = flush -":" = prompt -";" = bufferlist -@ = refresh -I = search tag:inbox AND NOT tag:killed -L = taglist -U = search tag:unread -\ = prompt search -d = bclose -m = compose -o = prompt search -q = exit -s = shell -shift tab = bprevious -tab = bnext - -[bufferlist-maps] -x = closefocussed -enter = openfocussed - -[search-maps] -& = toggletag killed -O = refineprompt -a = toggletag inbox -enter = openthread -l = retagprompt -| = refineprompt - -[envelope-maps] -enter = reedit -a = attach -s = prompt subject -t = prompt to -y = send - -[taglist-maps] -enter = select - -[thread-maps] -C = fold --all -E = unfold --all -H = toggleheaders -P = print --all -S = save --all -a = toggletag inbox -enter = select -f = forward -g = groupreply -p = print -r = reply -s = save -| = prompt pipeto - - -[command-aliases] -bn = bnext -bp = bprevious -clo = close -ls = bufferlist -quit = exit - - -[16c-theme] -bufferlist_focus_bg = dark gray -bufferlist_focus_fg = white -bufferlist_results_even_bg = black -bufferlist_results_even_fg = light gray -bufferlist_results_odd_bg = black -bufferlist_results_odd_fg = light gray -footer_bg = dark blue -footer_fg = light green -header_bg = dark blue -header_fg = white -message_attachment_bg = dark gray -message_attachment_fg = light gray -message_attachment_focussed_bg = light green -message_attachment_focussed_fg = light gray -message_body_bg = default -message_body_fg = light gray -message_header_bg = dark gray -message_header_fg = white -message_header_key_bg = dark gray -message_header_key_fg = white -message_header_value_bg = dark gray -message_header_value_fg = light gray -messagesummary_even_bg = light blue -messagesummary_even_fg = white -messagesummary_focus_bg = dark cyan -messagesummary_focus_fg = white -messagesummary_odd_bg = dark blue -messagesummary_odd_fg = white -notify_error_bg = dark red -notify_error_fg = white -notify_normal_bg = default -notify_normal_fg = default -prompt_bg = black -prompt_fg = light gray -tag_bg = black -tag_fg = brown -tag_focus_bg = dark cyan -tag_focus_fg = white -threadline_authors_bg = default -threadline_authors_fg = dark green -threadline_authors_focus_bg = dark cyan -threadline_authors_focus_fg = black,bold -threadline_bg = default -threadline_content_bg = default -threadline_content_fg = dark gray -threadline_date_bg = default -threadline_date_fg = light gray -threadline_date_focus_bg = dark cyan -threadline_date_focus_fg = black -threadline_fg = default -threadline_focus_bg = dark cyan -threadline_focus_fg = white -threadline_mailcount_bg = default -threadline_mailcount_fg = light gray -threadline_mailcount_focus_bg = dark cyan -threadline_mailcount_focus_fg = black -threadline_subject_bg = default -threadline_subject_fg = light gray -threadline_subject_focus_bg = dark cyan -threadline_subject_focus_fg = black -threadline_tags_bg = default -threadline_tags_fg = brown -threadline_tags_focus_bg = dark cyan -threadline_tags_focus_fg = yellow,bold - -[256c-theme] -bufferlist_focus_bg = g38 -bufferlist_focus_fg = #ffa -bufferlist_results_even_bg = g3 -bufferlist_results_even_fg = default -bufferlist_results_odd_bg = default -bufferlist_results_odd_fg = default -footer_bg = #006 -footer_fg = white -header_bg = dark blue -header_fg = white -message_attachment_bg = dark gray -message_attachment_fg = light gray -message_attachment_focussed_bg = light green -message_attachment_focussed_fg = light gray -message_body_bg = default -message_body_fg = light gray -message_header_bg = dark gray -message_header_fg = white -message_header_key_bg = dark gray -message_header_key_fg = white -message_header_value_bg = dark gray -message_header_value_fg = light gray -messagesummary_even_bg = #068 -messagesummary_even_fg = white -messagesummary_focus_bg = g58 -messagesummary_focus_fg = #ff8 -messagesummary_odd_bg = #006 -messagesummary_odd_fg = white -notify_error_bg = dark red -notify_error_fg = white -notify_normal_bg = default -notify_normal_fg = default -prompt_bg = default -prompt_fg = light gray -tag_bg = default -tag_fg = brown -tag_focus_bg = g58 -tag_focus_fg = #ffa -threadline_authors_bg = default -threadline_authors_fg = #6d6 -threadline_authors_focus_bg = g58 -threadline_authors_focus_fg = #8f6 -threadline_bg = default -threadline_content_bg = default -threadline_content_fg = #866 -threadline_date_bg = default -threadline_date_fg = g58 -threadline_date_focus_bg = g58 -threadline_date_focus_fg = g89 -threadline_fg = default -threadline_focus_bg = g58 -threadline_focus_fg = white -threadline_mailcount_bg = default -threadline_mailcount_fg = light gray -threadline_mailcount_focus_bg = g58 -threadline_mailcount_focus_fg = g89 -threadline_subject_bg = default -threadline_subject_fg = g58 -threadline_subject_focus_bg = g58 -threadline_subject_focus_fg = g89 -threadline_tags_bg = default -threadline_tags_fg = #a86 -threadline_tags_focus_bg = g58 -threadline_tags_focus_fg = #ff8 - -[1c-theme] -bufferlist_focus = standout -bufferlist_results_even = default -bufferlist_results_odd = default -footer = standout -header = standout -message_attachment = default -message_attachment_focussed = underline -message_body = default -message_header = default -message_header_key = default -message_header_value = default -messagesummary_even = -messagesummary_focus = standout -messagesummary_odd = -notify_error = standout -notify_normal = default -prompt = -tag = default -tag_focus = standout -threadline = default -threadline_authors = default,underline -threadline_authors_focus = standout -threadline_content = default -threadline_date = default -threadline_date_focus = standout -threadline_focus = standout -threadline_mailcount = default -threadline_mailcount_focus = standout -threadline_subject = default -threadline_subject_focus = standout -threadline_tags = bold -threadline_tags_focus = standout diff --git a/setup.py b/setup.py index 62b8fd27..301d6ee3 100755 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ setup(name='alot', author_email=alot.__author_email__, url=alot.__url__, packages=['alot'], + package_data={'alot': ['defaults/alot.rc', 'defaults/notmuch.rc']}, scripts=['bin/alot'], license=alot.__copyright__, requires=[ -- cgit v1.2.3 From f857a31527556a92d0453434c922e4801f417c1e Mon Sep 17 00:00:00 2001 From: Justus Winter <4winter@informatik.uni-hamburg.de> Date: Thu, 22 Sep 2011 14:00:27 +0200 Subject: Fix the config file parsing wrt to quoted values and : as key --- alot/defaults/alot.rc | 12 ++++++------ alot/settings.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/alot/defaults/alot.rc b/alot/defaults/alot.rc index fbe2b318..773a5bef 100644 --- a/alot/defaults/alot.rc +++ b/alot/defaults/alot.rc @@ -44,14 +44,14 @@ terminal_cmd = x-terminal-emulator -e timestamp_format = '' # how to print messages: -# this specifies a shellcommand used pro printing. +# this specifies a shellcommand used pro printing. # threads/messages are piped to this as plaintext. # muttprint/a2ps works nicely print_cmd = #initial searchstring when none is given as argument: initial_searchstring = tag:inbox AND NOT tag:killed - + # in case more than one account has an addressbook: # Set this to True to make tabcompletion for recipients during compose only # look in the abook of the account matching the sender address @@ -71,7 +71,7 @@ m = compose o = 'prompt search ' q = exit ';' = bufferlist -':' = prompt +colon = prompt [bufferlist-maps] x = closefocussed @@ -256,10 +256,10 @@ message_header_bg = dark gray [1c-theme] message_body = default message_header = default -prompt = +prompt = message_attachment = default notify_normal = default -messagesummary_odd = +messagesummary_odd = threadline_content = default threadline_subject_focus = standout bufferlist_results_odd = default @@ -279,7 +279,7 @@ bufferlist_results_even = default threadline_authors_focus = standout footer = standout notify_error = standout -messagesummary_even = +messagesummary_even = threadline_tags_focus = standout threadline = default threadline_date = default diff --git a/alot/settings.py b/alot/settings.py index 39783ba2..d7a682c6 100644 --- a/alot/settings.py +++ b/alot/settings.py @@ -18,6 +18,7 @@ Copyright (C) 2011 Patrick Totzke """ import imp import os +import ast import mailcap import codecs @@ -56,6 +57,24 @@ class AlotConfigParser(FallbackConfigParser): except: pass + # fix quoted keys / values + for section in self.sections(): + for key, value in self.items(section): + if value and value[0] in "\"'": + value = ast.literal_eval(value) + + transformed_key = False + if key[0] in "\"'": + transformed_key = ast.literal_eval(key) + elif key == 'colon': + transformed_key = ':' + + if transformed_key: + self.remove_option(section, key) + self.set(section, transformed_key, value) + else: + self.set(section, key, value) + def get_palette(self): mode = self.getint('general', 'colourmode') ms = "%dc-theme" % mode -- cgit v1.2.3 From 7aa14cee565777939411e70508c73f8688e9dbac Mon Sep 17 00:00:00 2001 From: Justus Winter <4winter@informatik.uni-hamburg.de> Date: Thu, 22 Sep 2011 11:54:08 +0200 Subject: Enhance external command templates Allow find style ('{}') command templates in the external command and securely quote the filename one has been given. --- alot/command.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/alot/command.py b/alot/command.py index d719ca81..046b50db 100644 --- a/alot/command.py +++ b/alot/command.py @@ -140,11 +140,13 @@ class RefreshCommand(Command): class ExternalCommand(Command): """calls external command""" - def __init__(self, commandstring, spawn=False, refocus=True, + def __init__(self, commandstring, path=None, spawn=False, refocus=True, in_thread=False, on_success=None, **kwargs): """ :param commandstring: the command to call :type commandstring: str + :param path: a path to a file (or None) + :type path: str :param spawn: run command in a new terminal :type spawn: boolean :param in_thread: run asynchronously, don't block alot @@ -155,6 +157,7 @@ class ExternalCommand(Command): :type on_success: callable """ self.commandstring = commandstring + self.path = path self.spawn = spawn self.refocus = refocus self.in_thread = in_thread @@ -174,7 +177,14 @@ class ExternalCommand(Command): write_fd = ui.mainloop.watch_pipe(afterwards) def thread_code(*args): - cmd = self.commandstring + if self.path: + if '{}' in self.commandstring: + cmd = self.commandstring.replace('{}', helper.shell_quote(self.path)) + else: + cmd = '%s %s' % (self.commandstring, helper.shell_quote(self.path)) + else: + cmd = self.commandstring + if self.spawn: cmd = '%s %s' % (settings.config.get('general', 'terminal_cmd'), @@ -202,8 +212,8 @@ class EditCommand(ExternalCommand): else: self.spawn = settings.config.getboolean('general', 'spawn_editor') editor_cmd = settings.config.get('general', 'editor_cmd') - cmd = editor_cmd + ' ' + self.path - ExternalCommand.__init__(self, cmd, spawn=self.spawn, + + ExternalCommand.__init__(self, editor_cmd, path=self.path, spawn=self.spawn, in_thread=self.spawn, **kwargs) -- cgit v1.2.3 From e7440c88368de4a9774d43d87e449fbf2709ba80 Mon Sep 17 00:00:00 2001 From: Justus Winter <4winter@informatik.uni-hamburg.de> Date: Thu, 22 Sep 2011 15:03:25 +0200 Subject: Adhere to the freedesktop basedir spec to locate the config file This patch retains the current behavior while also looking for the config in ~/.config/alot/config. For more details see: http://www.freedesktop.org/wiki/Specifications/basedir-spec --- alot/init.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/alot/init.py b/alot/init.py index 8d95fa72..77515b71 100755 --- a/alot/init.py +++ b/alot/init.py @@ -16,6 +16,7 @@ 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 . """ +import sys import argparse import logging import os @@ -30,7 +31,7 @@ from urwid.command_map import command_map def parse_args(): parser = argparse.ArgumentParser() parser.add_argument('-c', dest='configfile', - default='~/.alot.rc', + default=None, help='alot\'s config file') parser.add_argument('-n', dest='notmuchconfigfile', default='~/.notmuch-config', @@ -61,9 +62,27 @@ def main(): # interpret cml arguments args = parse_args() - #read config file - configfilename = os.path.expanduser(args.configfile) - settings.config.read(configfilename) + #locate and read config file + configfiles = [ + os.path.join(os.environ.get('XDG_CONFIG_HOME', + os.path.expanduser('~/.config')), + 'alot', 'config'), + os.path.expanduser('~/.alot.rc'), + ] + if args.configfile: + expanded_path = os.path.expanduser(args.configfile) + if not os.path.exists(expanded_path): + sys.exit('File %s does not exist' % expanded_path) + configfiles.insert(0, expanded_path) + + found_config = False + for configfilename in configfiles: + if os.path.exists(configfilename): + settings.config.read(configfilename) + found_config = True + if not found_config: + sys.exit('No configuration file found (tried %s)' % ', '.join(configfiles)) + notmuchfile = os.path.expanduser(args.notmuchconfigfile) settings.notmuchconfig.read(notmuchfile) settings.hooks.setup(settings.config.get('general', 'hooksfile')) -- cgit v1.2.3 From 505b4a72ec86ab8cd9458edccfbd708ed6f80752 Mon Sep 17 00:00:00 2001 From: Justus Winter <4winter@informatik.uni-hamburg.de> Date: Thu, 22 Sep 2011 15:10:43 +0200 Subject: Update the documentation --- USAGE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/USAGE.md b/USAGE.md index ef277a28..a6c9852d 100644 --- a/USAGE.md +++ b/USAGE.md @@ -64,8 +64,9 @@ Just like offlineimap or notmuch itself, alot reads a config file in the "INI" s It consists of some sections whose names are given in square brackets, followed by key-value pairs that use "=" or ":" as separator, ';' and '#' are comment-prefixes. -The default location for the config file is `~/.alot.rc`. -You can find a complete example config in `data/example.full.rc`. +The default location for the config file is `~/.config/alot/config`. +You can find a complete example config with the default values in +`alot/defaults/alot.rc`. Here is a key for the interpreted sections: [general] -- cgit v1.2.3 From be4a863672c8b93ecc678264485ba9ac8ec20951 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Thu, 22 Sep 2011 15:13:25 +0100 Subject: continue with defauls if no config found --- alot/init.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/alot/init.py b/alot/init.py index 77515b71..93c84149 100755 --- a/alot/init.py +++ b/alot/init.py @@ -80,8 +80,6 @@ def main(): if os.path.exists(configfilename): settings.config.read(configfilename) found_config = True - if not found_config: - sys.exit('No configuration file found (tried %s)' % ', '.join(configfiles)) notmuchfile = os.path.expanduser(args.notmuchconfigfile) settings.notmuchconfig.read(notmuchfile) -- cgit v1.2.3 From 511ceba4f7c157e8a6f19e17f33e2450a2256539 Mon Sep 17 00:00:00 2001 From: Justus Winter <4winter@informatik.uni-hamburg.de> Date: Thu, 22 Sep 2011 16:20:59 +0200 Subject: Update documentation --- USAGE.md | 51 +++++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/USAGE.md b/USAGE.md index ef277a28..69a2f028 100644 --- a/USAGE.md +++ b/USAGE.md @@ -6,57 +6,57 @@ to the prompt. Any commandline can be mapped by using the "MODE-maps" sections in the config file. These are the default keymaps: [global-maps] - $ = flush - : = prompt - ; = bufferlist @ = refresh I = search tag:inbox AND NOT tag:killed L = taglist + shift tab = bprevious U = search tag:unread - \ = prompt search + tab = bnext + \ = 'prompt search ' d = bclose + $ = flush m = compose - o = prompt search + o = 'prompt search ' q = exit - shift tab = bprevious - tab = bnext - + ';' = bufferlist + colon = prompt + [bufferlist-maps] - enter = openfocussed x = closefocussed - + enter = openfocussed + [search-maps] + a = toggletag inbox & = toggletag killed + l = retagprompt O = refineprompt - a = toggletag inbox enter = openthread - l = retagprompt | = refineprompt - + [envelope-maps] - a = attach - enter = reedit - s = prompt subject - t = prompt to + a = prompt attach ~/ y = send - + s = 'prompt subject ' + t = 'prompt to ' + enter = reedit + [taglist-maps] enter = select - + [thread-maps] C = fold --all E = unfold --all H = toggleheaders - P = print --all + P = print --thread S = save --all a = toggletag inbox - enter = select - f = forward g = groupreply + f = forward p = print - r = reply s = save - | = prompt pipeto + r = reply + enter = select + | = 'prompt pipeto ' Config ------ @@ -64,6 +64,9 @@ Just like offlineimap or notmuch itself, alot reads a config file in the "INI" s It consists of some sections whose names are given in square brackets, followed by key-value pairs that use "=" or ":" as separator, ';' and '#' are comment-prefixes. +Note that since ":" is a separator for key-value pairs you need to use "colon" to bind +commands to ":". + The default location for the config file is `~/.alot.rc`. You can find a complete example config in `data/example.full.rc`. Here is a key for the interpreted sections: -- cgit v1.2.3 From 4058091055e081b42a55f3d4ec94ddd36ed6e697 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Thu, 22 Sep 2011 15:25:56 +0100 Subject: updated NEWS --- NEWS | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index 5cc50c35..a8c6373e 100644 --- a/NEWS +++ b/NEWS @@ -1,13 +1,15 @@ 0.12 +* moved default location for config to ~/.config/alot/config +* hooks for pre/post editing and RE/FWD quotestrings * recipient completion gives priority to abook of sender account * smarter in-string-tabcompletion * added ability to pipe messages/treads to custom shellcommands * initial searchstring configurable in configfile -* prompt non-blocking (new syntax for prompts!) +* non-blocking prompt/choice (new syntax for prompts!) * fix attachment saving -Thanks: Ruben Pollan, Luke Macken +Thanks: Ruben Pollan, Luke Macken, Justus Winter 0.11 -- cgit v1.2.3 From 1d14e9d06513c1c3072ed7057aa0d7d750229579 Mon Sep 17 00:00:00 2001 From: Justus Winter <4winter@informatik.uni-hamburg.de> Date: Thu, 22 Sep 2011 16:37:55 +0200 Subject: Fix default configuration Fix a spurious single quote that snuck in from example.full.rc and make the empty print_cmd line consistent with the timestamp_format entry. --- alot/defaults/alot.rc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alot/defaults/alot.rc b/alot/defaults/alot.rc index 773a5bef..6f453d6e 100644 --- a/alot/defaults/alot.rc +++ b/alot/defaults/alot.rc @@ -20,7 +20,7 @@ displayed_headers = From,To,Cc,Bcc,Subject # editor command editor_cmd = /usr/bin/vim -f -c 'set filetype=mail' + -editor_writes_encoding' = UTF-8 +editor_writes_encoding = UTF-8 # timeout in secs after a failed attempt to flush is repeated flush_retry_timeout = 5 @@ -47,7 +47,7 @@ timestamp_format = '' # this specifies a shellcommand used pro printing. # threads/messages are piped to this as plaintext. # muttprint/a2ps works nicely -print_cmd = +print_cmd = '' #initial searchstring when none is given as argument: initial_searchstring = tag:inbox AND NOT tag:killed -- cgit v1.2.3 From ba7a1eef779c7c8b65c4c6733cb4a30ecf744694 Mon Sep 17 00:00:00 2001 From: Justus Winter <4winter@informatik.uni-hamburg.de> Date: Thu, 22 Sep 2011 16:44:50 +0200 Subject: Set the default location of the hooks file to ~/.config/alot/hooks.py Just to encourage everyone to use the nice layout suggested by the fdo spec. --- alot/defaults/alot.rc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alot/defaults/alot.rc b/alot/defaults/alot.rc index 773a5bef..0b700501 100644 --- a/alot/defaults/alot.rc +++ b/alot/defaults/alot.rc @@ -26,7 +26,7 @@ editor_writes_encoding' = UTF-8 flush_retry_timeout = 5 # where to look up hooks -hooksfile = ~/.alot.py +hooksfile = ~/.config/alot/hooks.py # time in secs to display status messages notify_timeout = 2 -- cgit v1.2.3 From b89dd7a3927824d801756b3e220104b2dfa46745 Mon Sep 17 00:00:00 2001 From: Justus Winter <4winter@informatik.uni-hamburg.de> Date: Thu, 22 Sep 2011 17:00:47 +0200 Subject: Update the documentation --- USAGE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/USAGE.md b/USAGE.md index a6c9852d..c3247599 100644 --- a/USAGE.md +++ b/USAGE.md @@ -134,7 +134,8 @@ You can tune this using the `abook_regexp` option (beware Commandparsers escapin Hooks ----- Hooks are python callables that live in a module specified by -`hooksfile` in the `[global]` section of your config. Per default this points to `~/.alot.py`. +`hooksfile` in the `[global]` section of your config. Per default this points +to `~/.config/alot/hooks.py`. For every command X, the callable 'pre_X' will be called before X and 'post_X' afterwards. When a hook gets called, it receives instances of -- cgit v1.2.3 From 6a32255f17e3b1e2e61b437456fea904535a6ec5 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Fri, 23 Sep 2011 16:58:28 +0100 Subject: pep8 cleanup --- alot/command.py | 10 ++++++---- alot/helper.py | 1 + alot/settings.py | 5 ++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/alot/command.py b/alot/command.py index 046b50db..e491d583 100644 --- a/alot/command.py +++ b/alot/command.py @@ -179,9 +179,11 @@ class ExternalCommand(Command): def thread_code(*args): if self.path: if '{}' in self.commandstring: - cmd = self.commandstring.replace('{}', helper.shell_quote(self.path)) + cmd = self.commandstring.replace('{}', + helper.shell_quote(self.path)) else: - cmd = '%s %s' % (self.commandstring, helper.shell_quote(self.path)) + cmd = '%s %s' % (self.commandstring, + helper.shell_quote(self.path)) else: cmd = self.commandstring @@ -213,8 +215,8 @@ class EditCommand(ExternalCommand): self.spawn = settings.config.getboolean('general', 'spawn_editor') editor_cmd = settings.config.get('general', 'editor_cmd') - ExternalCommand.__init__(self, editor_cmd, path=self.path, spawn=self.spawn, - in_thread=self.spawn, + ExternalCommand.__init__(self, editor_cmd, path=self.path, + spawn=self.spawn, in_thread=self.spawn, **kwargs) diff --git a/alot/helper.py b/alot/helper.py index d7916123..91da0811 100644 --- a/alot/helper.py +++ b/alot/helper.py @@ -114,6 +114,7 @@ def attach(path, mail, filename=None): filename=filename) mail.attach(part) + def shell_quote(text): r''' >>> print(shell_quote("hello")) diff --git a/alot/settings.py b/alot/settings.py index d7a682c6..5afd4463 100644 --- a/alot/settings.py +++ b/alot/settings.py @@ -24,6 +24,7 @@ import codecs from ConfigParser import SafeConfigParser + class FallbackConfigParser(SafeConfigParser): def __init__(self): SafeConfigParser.__init__(self) @@ -151,7 +152,9 @@ class HookManager(object): config = AlotConfigParser() config.read(os.path.join(os.path.dirname(__file__), 'defaults', 'alot.rc')) notmuchconfig = FallbackConfigParser() -notmuchconfig.read(os.path.join(os.path.dirname(__file__), 'defaults', 'notmuch.rc')) +notmuchconfig.read(os.path.join(os.path.dirname(__file__), + 'defaults', + 'notmuch.rc')) hooks = HookManager() mailcaps = mailcap.getcaps() -- cgit v1.2.3