diff options
author | Patrick Totzke <patricktotzke@gmail.com> | 2012-02-23 22:11:35 +0000 |
---|---|---|
committer | Patrick Totzke <patricktotzke@gmail.com> | 2012-02-23 22:11:35 +0000 |
commit | 5d9448b5d262302225d775c0b3c38e6369ea19dc (patch) | |
tree | aa5a94bdee27f3eaf2dcec3379668fb8c4bcc560 | |
parent | 45cfa1ae28c783ff7ad23f74978c2d9d062c1fef (diff) | |
parent | 6667f882da353654749d6073dc28af9aad84116a (diff) |
Merge branch 'rewrite-configs' into testing
33 files changed, 1905 insertions, 1508 deletions
diff --git a/alot/account.py b/alot/account.py index 106acc46..d15e7f2a 100644 --- a/alot/account.py +++ b/alot/account.py @@ -49,12 +49,11 @@ class Account(object): gpg_key=None, signature=None, signature_filename=None, signature_as_attachment=False, sent_box=None, sent_tags=['sent'], draft_box=None, draft_tags=['draft'], - abook=None): + abook=None, **rest): self.address = address self.abook = abook self.aliases = [] - if aliases: - self.aliases = aliases.split(';') + self.aliases = aliases self.realname = realname self.gpg_key = gpg_key self.signature = signature @@ -195,124 +194,6 @@ class SendmailAccount(Account): return d -class AccountManager(object): - """ - creates and organizes multiple :class:`Accounts <Account>` that were - defined in the "account" sections of a given - :class:`~alot.settings.AlotConfigParser`. - """ - allowed = ['realname', - 'address', - 'aliases', - 'gpg_key', - 'signature', - 'signature_filename', - 'signature_as_attachment', - 'type', - 'sendmail_command', - 'abook_command', - 'abook_regexp', - 'sent_box', - 'sent_tags', - 'draft_box', - 'draft_tags'] - manditory = ['realname', 'address'] - parse_lists = ['sent_tags', 'draft_tags'] - accountmap = {} - accounts = [] - ordered_addresses = [] - - def __init__(self, config): - """ - :param config: the config object to read account information from - :type config: :class:`~alot.settings.AlotConfigParser`. - """ - sections = config.sections() - 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: - regexp = config.get(s, 'abook_regexp') - options.remove('abook_regexp') - else: - regexp = None # will use default in constructor - args['abook'] = MatchSdtoutAddressbook(cmd, match=regexp) - - if 'signature_as_attachment' in options: - value = config.getboolean(s, 'signature_as_attachment') - args['signature_as_attachment'] = value - options.remove('signature_as_attachment') - - to_set = self.manditory - for o in options: - if o not in self.parse_lists: - args[o] = config.get(s, o) - else: - args[o] = config.getstringlist(s, o) - if o in to_set: - to_set.remove(o) # removes obligation - if not to_set: # all manditory fields were present - sender_type = args.pop('type', 'sendmail') - if sender_type == 'sendmail': - cmd = args.pop('sendmail_command', 'sendmail') - newacc = (SendmailAccount(cmd, **args)) - self.accountmap[newacc.address] = newacc - self.accounts.append(newacc) - for alias in newacc.aliases: - self.accountmap[alias] = newacc - else: - logging.info('account section %s lacks %s' % (s, to_set)) - - def get_accounts(self): - """ - returns known accounts - - :rtype: list of :class:`Account` - """ - return self.accounts - - def get_account_by_address(self, address): - """ - returns :class:`Account` for a given email address (str) - - :param address: address to look up - :type address: string - :rtype: :class:`Account` or None - """ - - for myad in self.get_addresses(): - if myad in address: - return self.accountmap[myad] - return None - - def get_main_addresses(self): - """returns addresses of known accounts without its aliases""" - return [a.address for a in self.accounts] - - def get_addresses(self): - """returns addresses of known accounts including all their aliases""" - return self.accountmap.keys() - - def get_addressbooks(self, order=[], append_remaining=True): - """returns list of all defined :class:`AddressBook` objects""" - 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: - abooks.append(a.abook) - return abooks - - class AddressBook(object): """can look up email addresses and realnames for contacts. diff --git a/alot/buffers.py b/alot/buffers.py index 407dc47e..f8d70de3 100644 --- a/alot/buffers.py +++ b/alot/buffers.py @@ -2,7 +2,7 @@ import urwid from notmuch import NotmuchError import widgets -import settings +from settings import settings import commands from walker import PipeWalker from helper import shorten_author_string @@ -70,10 +70,13 @@ class BufferlistBuffer(Buffer): for (num, b) in enumerate(displayedbuffers): line = widgets.BufferlineWidget(b) if (num % 2) == 0: - attr = 'bufferlist_results_even' + attr = settings.get_theming_attribute('bufferlist', + 'results_even') else: - attr = 'bufferlist_results_odd' - buf = urwid.AttrMap(line, attr, 'bufferlist_focus') + attr = settings.get_theming_attribute('bufferlist', + 'results_odd') + focus_att = settings.get_theming_attribute('bufferlist', 'focus') + buf = urwid.AttrMap(line, attr, focus_att) num = urwid.Text('%3d:' % self.index_of(b)) lines.append(urwid.Columns([('fixed', 4, num), buf])) self.bufferlist = urwid.ListBox(urwid.SimpleListWalker(lines)) @@ -105,8 +108,7 @@ class EnvelopeBuffer(Buffer): def rebuild(self): displayed_widgets = [] - hidden = settings.config.getstringlist('general', - 'envelope_headers_blacklist') + hidden = settings.get('envelope_headers_blacklist') #build lines lines = [] for (k, vlist) in self.envelope.headers.items(): @@ -145,8 +147,7 @@ class SearchBuffer(Buffer): self.dbman = ui.dbman self.ui = ui self.querystring = initialquery - default_order = settings.config.get('general', - 'search_threads_sort_order') + default_order = settings.get('search_threads_sort_order') self.sort_order = sort_order or default_order self.result_count = 0 self.isinitialized = False diff --git a/alot/commands/__init__.py b/alot/commands/__init__.py index 76bbd63a..ff9b7951 100644 --- a/alot/commands/__init__.py +++ b/alot/commands/__init__.py @@ -5,7 +5,7 @@ import shlex import logging import argparse -import alot.settings +from alot.settings import settings import alot.helper @@ -177,8 +177,7 @@ def commandfactory(cmdline, mode='global'): args = args[1:] # unfold aliases - if alot.settings.config.has_option('command-aliases', cmdname): - cmdname = alot.settings.config.get('command-aliases', cmdname) + # TODO: read from settingsmanager # get class, argparser and forced parameter (cmdclass, parser, forcedparms) = lookup_command(cmdname, mode) @@ -192,7 +191,7 @@ def commandfactory(cmdline, mode='global'): logging.debug('PARMS: %s' % parms) # set pre and post command hooks - get_hook = alot.settings.config.get_hook + get_hook = settings.get_hook parms['prehook'] = get_hook('pre_%s_%s' % (mode, cmdname)) or \ get_hook('pre_global_%s' % cmdname) parms['posthook'] = get_hook('post_%s_%s' % (mode, cmdname)) or \ diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py index 81544629..cc67a5a1 100644 --- a/alot/commands/envelope.py +++ b/alot/commands/envelope.py @@ -5,16 +5,15 @@ import logging import email import tempfile from twisted.internet.defer import inlineCallbacks -from twisted.internet import threads import datetime from alot.account import SendingMailFailed from alot import buffers from alot import commands from alot.commands import Command, registerCommand -from alot import settings from alot.commands import globals from alot.helper import string_decode +from alot.settings import settings MODE = 'envelope' @@ -77,13 +76,13 @@ class SaveCommand(Command): # determine account to use sname, saddr = email.Utils.parseaddr(envelope.get('From')) - account = ui.accountman.get_account_by_address(saddr) + account = settings.get_account_by_address(saddr) if account == None: - if not ui.accountman.get_accounts(): + if not settings.get_accounts(): ui.notify('no accounts set.', priority='error') return else: - account = ui.accountman.get_accounts()[0] + account = settings.get_accounts()[0] if account.draft_box == None: ui.notify('abort: account <%s> has no draft_box set.' % saddr, @@ -121,16 +120,15 @@ class SendCommand(Command): return frm = envelope.get('From') sname, saddr = email.Utils.parseaddr(frm) - omit_signature = False # determine account to use for sending - account = ui.accountman.get_account_by_address(saddr) + account = settings.get_account_by_address(saddr) if account == None: - if not ui.accountman.get_accounts(): + if not settings.get_accounts(): ui.notify('no accounts set', priority='error') return else: - account = ui.accountman.get_accounts()[0] + account = settings.get_accounts()[0] # send clearme = ui.notify('sending..', timeout=-1) @@ -193,16 +191,14 @@ class EditCommand(Command): self.envelope = ui.current_buffer.envelope #determine editable headers - edit_headers = set(settings.config.getstringlist('general', - 'edit_headers_whitelist')) + edit_headers = set(settings.get('edit_headers_whitelist')) if '*' in edit_headers: edit_headers = set(self.envelope.headers.keys()) - blacklist = set(settings.config.getstringlist('general', - 'edit_headers_blacklist')) + blacklist = set(settings.get('edit_headers_blacklist')) if '*' in blacklist: blacklist = set(self.envelope.headers.keys()) self.edit_headers = edit_headers - blacklist - logging.info('editable headers: %s' % blacklist) + logging.info('editable headers: %s' % self.edit_headers) def openEnvelopeFromTmpfile(): # This parses the input from the tempfile. @@ -213,15 +209,14 @@ class EditCommand(Command): # get input f = open(tf.name) os.unlink(tf.name) - enc = settings.config.get('general', 'editor_writes_encoding') + enc = settings.get('editor_writes_encoding') template = string_decode(f.read(), enc) f.close() # call post-edit translate hook - translate = settings.config.get_hook('post_edit_translate') + translate = settings.get_hook('post_edit_translate') if translate: - template = translate(template, ui=ui, dbm=ui.dbman, - aman=ui.accountman, config=settings.config) + template = translate(template, ui=ui, dbm=ui.dbman) self.envelope.parse_template(template) if self.openNew: ui.buffer_open(buffers.EnvelopeBuffer(ui, self.envelope)) @@ -246,10 +241,9 @@ class EditCommand(Command): bodytext = self.envelope.body # call pre-edit translate hook - translate = settings.config.get_hook('pre_edit_translate') + translate = settings.get_hook('pre_edit_translate') if translate: - bodytext = translate(bodytext, ui=ui, dbm=ui.dbman, - aman=ui.accountman, config=settings.config) + bodytext = translate(bodytext, ui=ui, dbm=ui.dbman) #write stuff to tempfile tf = tempfile.NamedTemporaryFile(delete=False) diff --git a/alot/commands/globals.py b/alot/commands/globals.py index 81385456..db944a00 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -15,7 +15,6 @@ from alot.completion import CommandLineCompleter from alot.commands import CommandParseError from alot.commands import commandfactory from alot import buffers -from alot import settings from alot import widgets from alot import helper from alot.db import DatabaseLockedError @@ -23,6 +22,7 @@ from alot.completion import ContactsCompleter from alot.completion import AccountCompleter from alot.message import Envelope from alot import commands +from alot.settings import settings MODE = 'global' @@ -32,7 +32,7 @@ class ExitCommand(Command): """shut down cleanly""" @inlineCallbacks def apply(self, ui): - if settings.config.getboolean('general', 'bug_on_exit'): + if settings.get('bug_on_exit'): if (yield ui.choice('realy quit?', select='yes', cancel='no', msg_position='left')) == 'no': return @@ -91,7 +91,6 @@ class PromptCommand(Command): cmdline = yield ui.prompt(prefix=':', text=self.startwith, completer=CommandLineCompleter(ui.dbman, - ui.accountman, mode, ui.current_buffer, ), @@ -178,9 +177,7 @@ class ExternalCommand(Command): cmd = self.commandstring if self.spawn: - cmd = '%s %s' % (settings.config.get('general', - 'terminal_cmd'), - cmd) + cmd = '%s %s' % (settings.get('terminal_cmd'), cmd) cmd = cmd.encode('utf-8', errors='ignore') logging.info('calling external command: %s' % cmd) try: @@ -219,19 +216,17 @@ class EditCommand(ExternalCommand): if spawn != None: self.spawn = spawn else: - self.spawn = settings.config.getboolean('general', 'editor_spawn') + self.spawn = settings.get('editor_spawn') if thread != None: self.thread = thread else: - self.thread = settings.config.getboolean('general', - 'editor_in_thread') + self.thread = settings.get('editor_in_thread') self.editor_cmd = None if os.path.isfile('/usr/bin/editor'): self.editor_cmd = '/usr/bin/editor' self.editor_cmd = os.environ.get('EDITOR', self.editor_cmd) - self.editor_cmd = settings.config.get('general', 'editor_cmd', - fallback=self.editor_cmd) + self.editor_cmd = settings.get('editor_cmd') or self.editor_cmd logging.debug('using editor_cmd: %s' % self.editor_cmd) ExternalCommand.__init__(self, self.editor_cmd, path=self.path, @@ -269,7 +264,7 @@ class BufferCloseCommand(Command): if self.buffer == None: self.buffer = ui.current_buffer if len(ui.buffers) == 1: - if settings.config.getboolean('general', 'quit_on_last_bclose'): + if settings.get('quit_on_last_bclose'): logging.info('closing the last buffer, exiting') ui.apply_command(ExitCommand()) else: @@ -353,7 +348,7 @@ class FlushCommand(Command): try: ui.dbman.flush() except DatabaseLockedError: - timeout = settings.config.getint('general', 'flush_retry_timeout') + timeout = settings.get('flush_retry_timeout') def f(*args): self.apply(ui) @@ -383,8 +378,8 @@ class HelpCommand(Command): logging.debug('HELP') if self.commandname == 'bindings': # get mappings - modemaps = dict(settings.config.items('%s-maps' % ui.mode)) - globalmaps = dict(settings.config.items('global-maps')) + modemaps = dict(settings._bindings[ui.mode].items()) + globalmaps = dict(settings._bindings['global'].items()) # build table maxkeylength = len(max((modemaps).keys() + globalmaps.keys(), @@ -414,9 +409,11 @@ class HelpCommand(Command): ckey = 'cancel' titletext = 'Bindings Help (%s cancels)' % ckey + text_att = settings.get_theming_attribute('help', 'text') + title_att = settings.get_theming_attribute('help', 'title') box = widgets.DialogBox(body, titletext, - bodyattr='help_text', - titleattr='help_title') + bodyattr=text_att, + titleattr=title_att) # put promptwidget as overlay on main widget overlay = urwid.Overlay(box, ui.mainframe, 'center', @@ -499,7 +496,7 @@ class ComposeCommand(Command): self.envelope = Envelope() if self.template is not None: #get location of tempsdir, containing msg templates - tempdir = settings.config.get('general', 'template_dir') + tempdir = settings.get('template_dir') tempdir = os.path.expanduser(tempdir) if not tempdir: xdgdir = os.environ.get('XDG_CONFIG_HOME', @@ -542,19 +539,19 @@ class ComposeCommand(Command): # get missing From header if not 'From' in self.envelope.headers: - accounts = ui.accountman.get_accounts() + accounts = settings.get_accounts() if len(accounts) == 1: a = accounts[0] fromstring = "%s <%s>" % (a.realname, a.address) self.envelope.add('From', fromstring) else: - cmpl = AccountCompleter(ui.accountman) + cmpl = AccountCompleter() fromaddress = yield ui.prompt(prefix='From>', completer=cmpl, tab=1) if fromaddress is None: ui.notify('canceled') return - a = ui.accountman.get_account_by_address(fromaddress) + a = settings.get_account_by_address(fromaddress) if a is not None: fromstring = "%s <%s>" % (a.realname, a.address) self.envelope.add('From', fromstring) @@ -564,7 +561,7 @@ class ComposeCommand(Command): # add signature if not self.omit_signature: name, addr = email.Utils.parseaddr(self.envelope['From']) - account = ui.accountman.get_account_by_address(addr) + account = settings.get_account_by_address(addr) if account is not None: if account.signature: logging.debug('has signature') @@ -594,13 +591,12 @@ class ComposeCommand(Command): if 'To' not in self.envelope.headers: sender = self.envelope.get('From') name, addr = email.Utils.parseaddr(sender) - account = ui.accountman.get_account_by_address(addr) + account = settings.get_account_by_address(addr) - allbooks = not settings.config.getboolean('general', - 'complete_matching_abook_only') + allbooks = not settings.get('complete_matching_abook_only') logging.debug(allbooks) if account is not None: - abooks = ui.accountman.get_addressbooks(order=[account], + abooks = settings.get_addressbooks(order=[account], append_remaining=allbooks) logging.debug(abooks) completer = ContactsCompleter(abooks) @@ -613,7 +609,7 @@ class ComposeCommand(Command): return self.envelope.add('To', to) - if settings.config.getboolean('general', 'ask_subject') and \ + if settings.get('ask_subject') and \ not 'Subject' in self.envelope.headers: subject = yield ui.prompt(prefix='Subject>') logging.debug('SUBJECT: "%s"' % subject) diff --git a/alot/commands/thread.py b/alot/commands/thread.py index 31b4b19f..c3a5da8d 100644 --- a/alot/commands/thread.py +++ b/alot/commands/thread.py @@ -12,7 +12,6 @@ from alot.commands.globals import ExternalCommand from alot.commands.globals import FlushCommand from alot.commands.globals import ComposeCommand from alot.commands.globals import RefreshCommand -from alot import settings from alot import widgets from alot import completion from alot.message import decode_header @@ -21,7 +20,7 @@ from alot.message import extract_headers from alot.message import extract_body from alot.message import Envelope from alot.db import DatabaseROError -from alot.db import DatabaseError +from alot.settings import settings MODE = 'thread' @@ -82,7 +81,7 @@ class ReplyCommand(Command): def apply(self, ui): # look if this makes sense: do we have any accounts set up? - my_accounts = ui.accountman.get_accounts() + my_accounts = settings.get_accounts() if not my_accounts: ui.notify('no accounts set', priority='error') return @@ -95,11 +94,9 @@ class ReplyCommand(Command): # set body text name, address = self.message.get_author() timestamp = self.message.get_date() - qf = settings.config.get_hook('reply_prefix') + qf = settings.get_hook('reply_prefix') if qf: - quotestring = qf(name, address, timestamp, - ui=ui, dbm=ui.dbman, aman=ui.accountman, - config=settings.config) + quotestring = qf(name, address, timestamp, ui=ui, dbm=ui.dbman) else: quotestring = 'Quoting %s (%s)\n' % (name, timestamp) mailcontent = quotestring @@ -121,7 +118,7 @@ class ReplyCommand(Command): # set To sender = mail['Reply-To'] or mail['From'] recipients = [sender] - my_addresses = ui.accountman.get_addresses() + my_addresses = settings.get_addresses() if self.groupreply: if sender != mail['From']: recipients.append(mail['From']) @@ -180,7 +177,7 @@ class ForwardCommand(Command): def apply(self, ui): # look if this makes sense: do we have any accounts set up? - my_accounts = ui.accountman.get_accounts() + my_accounts = settings.get_accounts() if not my_accounts: ui.notify('no accounts set', priority='error') return @@ -196,11 +193,9 @@ class ForwardCommand(Command): # set body text name, address = self.message.get_author() timestamp = self.message.get_date() - qf = settings.config.get_hook('forward_prefix') + qf = settings.get_hook('forward_prefix') if qf: - quote = qf(name, address, timestamp, - ui=ui, dbm=ui.dbman, aman=ui.accountman, - config=settings.config) + quote = qf(name, address, timestamp, ui=ui, dbm=ui.dbman) else: quote = 'Forwarded message from %s (%s):\n' % (name, timestamp) mailcontent = quote @@ -548,7 +543,7 @@ class PrintCommand(PipeCommand): :type add_tags: bool """ # get print command - cmd = settings.config.get('general', 'print_cmd', fallback='') + cmd = settings.get('print_cmd', fallback='') # set up notification strings if all: diff --git a/alot/completion.py b/alot/completion.py index 94ab33c0..e9e44226 100644 --- a/alot/completion.py +++ b/alot/completion.py @@ -6,6 +6,7 @@ import argparse import alot.commands as commands from alot.buffers import EnvelopeBuffer +from alot.settings import settings class Completer(object): @@ -42,7 +43,7 @@ class StringlistCompleter(Completer): def __init__(self, resultlist): """ :param resultlist: strings used for completion - :type accountman: list of str + :type resultlist: list of str """ self.resultlist = resultlist @@ -97,16 +98,13 @@ class MultipleSelectionCompleter(Completer): class QueryCompleter(Completer): """completion for a notmuch query string""" - def __init__(self, dbman, accountman): + def __init__(self, dbman): """ :param dbman: used to look up avaliable tagstrings :type dbman: :class:`~alot.db.DBManager` - :param accountman: used to look up known addresses to complete 'from' - and 'to' queries - :type accountman: :class:`~alot.account.AccountManager` """ self.dbman = dbman - abooks = accountman.get_addressbooks() + abooks = settings.get_addressbooks() self._abookscompleter = AbooksCompleter(abooks, addressesonly=True) self._tagcompleter = TagCompleter(dbman) self.keywords = ['tag', 'from', 'to', 'subject', 'attachment', @@ -210,6 +208,7 @@ class AbooksCompleter(Completer): returnlist.append((newtext, len(newtext))) return returnlist + class ArgparseOptionCompleter(Completer): """completes option parameters for a given argparse.Parser""" def __init__(self, parser): @@ -226,7 +225,7 @@ class ArgparseOptionCompleter(Completer): res = [] for act in self.actions: if '=' in pref: - optionstring = pref[:pref.rfind('=')+1] + optionstring = pref[:pref.rfind('=') + 1] # get choices if 'choices' in act.__dict__: choices = act.choices or [] @@ -241,15 +240,12 @@ class ArgparseOptionCompleter(Completer): return [(a, len(a)) for a in res] + class AccountCompleter(StringlistCompleter): """completes users' own mailaddresses""" - def __init__(self, accountman): - """ - :param accountman: used to look up the list of addresses - :type accountman: :class:`~alot.account.AccountManager` - """ - resultlist = accountman.get_main_addresses() + def __init__(self): + resultlist = settings.get_main_addresses() StringlistCompleter.__init__(self, resultlist) @@ -276,13 +272,10 @@ class CommandCompleter(Completer): class CommandLineCompleter(Completer): """completion for commandline""" - def __init__(self, dbman, accountman, mode, currentbuffer=None): + def __init__(self, dbman, mode, currentbuffer=None): """ :param dbman: used to look up avaliable tagstrings :type dbman: :class:`~alot.db.DBManager` - :param accountman: used to look up known addresses to complete 'from' - and 'to' queries - :type accountman: :class:`~alot.account.AccountManager` :param mode: mode identifier :type mode: str :param currentbuffer: currently active buffer. If defined, this will be @@ -291,13 +284,12 @@ class CommandLineCompleter(Completer): :type currentbuffer: :class:`~alot.buffers.Buffer` """ self.dbman = dbman - self.accountman = accountman self.mode = mode self.currentbuffer = currentbuffer self._commandcompleter = CommandCompleter(mode) - self._querycompleter = QueryCompleter(dbman, accountman) + self._querycompleter = QueryCompleter(dbman) self._tagcompleter = TagCompleter(dbman) - abooks = accountman.get_addressbooks() + abooks = settings.get_addressbooks() self._contactscompleter = ContactsCompleter(abooks) self._pathcompleter = PathCompleter() @@ -7,7 +7,7 @@ from datetime import datetime from collections import deque from message import Message -from settings import notmuchconfig as config +from settings import settings DB_ENC = 'utf-8' @@ -94,7 +94,7 @@ class DBManager(object): raise DatabaseLockedError() # read notmuch's config regarding imap flag synchronization - sync = config.getboolean('maildir', 'synchronize_flags') + sync = settings.get_notmuch_setting('maildir', 'synchronize_flags') # go through writequeue entries while self.writequeue: diff --git a/alot/defaults/alot.rc b/alot/defaults/alot.rc deleted file mode 100644 index d6dfa0c3..00000000 --- a/alot/defaults/alot.rc +++ /dev/null @@ -1,437 +0,0 @@ -[general] - -# ask for subject when compose -ask_subject = True - -# confirm exit -bug_on_exit = False - -# offset of next focussed buffer if the current one gets closed -bufferclose_focus_offset=-1 - -# number of colours your terminal supports -colourmode = 256 - -# number of spaces used to replace tab characters -tabwidth = 8 - -# templates directory that contains your message templates. -# It will be used if you give `compose --template` a filename without a path prefix. -# This defaults to `$XDG_CONFIG_HOME/alot/templates` if unset. -template_dir = - -# fill threadline with message content -display_content_in_threadline = False - -# headers that get displayed by default -displayed_headers = From,To,Cc,Bcc,Subject - -# headers that are hidden in envelope buffers by default -envelope_headers_blacklist = In-Reply-To,References - -# set terminal command used for spawning shell commands -terminal_cmd = /usr/bin/x-terminal-emulator -e - -#################### -# EDITOR settings # -#################### -# editor command -# if unset, alot will first try the EDITOR env variable, then /usr/bin/editor -#editor_cmd = /usr/bin/vim -f -c 'set filetype=mail' + - -# file encoding used by your editor -editor_writes_encoding = UTF-8 - -# use terminal_command to spawn a new terminal for the editor? -editor_spawn = False - -# call editor in separate thread. -# in case your editor doesn't run in the same window as alot, setting true here -# will make alot non-blocking during edits -editor_in_thread = False - - -# Which header fields should be editable in your editor -# used are those that match the whitelist and don't macht the blacklist. -# in both cases '*' may be used to indicate all fields. -edit_headers_whitelist = * -edit_headers_blacklist = Content-Type,MIME-Version,References,In-Reply-To - - -# timeout in secs after a failed attempt to flush is repeated -flush_retry_timeout = 5 - -# where to look up hooks -hooksfile = ~/.config/alot/hooks.py - -# time in secs to display status messages -notify_timeout = 2 - -# display statusline? -show_statusbar = True - -# strftime format for timestamps. Note: you must escape % twice here: -# use '%%%%' instead of '%' (and use '%%%%%%%%' to get a literal '%'). -# for the strftime format, see -# http://docs.python.org/library/datetime.html#strftime-strptime-behavior -# timestamp_format = '' - -# max length of authors line in thread widgets -authors_maxlength = 30 - -# 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 command when none is given as argument: -initial_command = search tag:inbox AND NOT tag:killed - -# default sort order of results in a search -# must be one of one of 'oldest_first', 'newest_first', 'message_id' or 'unsorted' -search_threads_sort_order = newest_first - -# in case more than one account has an address book: -# 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 - -# shut down when the last buffer gets closed -quit_on_last_bclose = False - -# value of the User-Agent header used for outgoing mails. -# setting this to the empty string will cause alot to omit the header all together. -# The string '%(version)s' will be replaced by the version string of the running instance. -# Beware ne necessary 3-fold escaping for the symbol '%' though. -user_agent = 'alot/%%%%%%%%(version)s' - - -[global-maps] -j = move down -k = move up -' ' = move page down -esc = cancel -enter = select - -@ = refresh -? = help bindings -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 -colon = prompt - -[bufferlist-maps] -x = close -select = openfocussed - -[search-maps] -a = toggletags inbox -& = toggletags killed -! = toggletags flagged -s = toggletags unread -l = retagprompt -O = refineprompt -| = refineprompt - -[envelope-maps] -a = prompt 'attach ~/' -y = send -P = save -s = 'refine Subject' -t = 'refine To' -b = 'refine Bcc' -c = 'refine Cc' -select = edit -H = toggleheaders - -[taglist-maps] - -[thread-maps] -C = fold --all -E = unfold --all -c = fold -e = unfold -< = fold -> = unfold -H = toggleheaders -h = togglesource -P = print --all --separately --add_tags -S = save --all -g = reply --all -f = forward -p = print --add_tags -n = editnew -s = save -r = reply -| = prompt 'pipeto ' - -[command-aliases] -quit = exit -bn = bnext -clo = close -bp = bprevious -ls = bufferlist - -[highlighting] -# Thread lines in the search buffer can be highlighted if they match a query -# by theming their components. - -# dictionary of highlighting rules. The keys are queries you want highlighting -# for; values are chosen designators that identify themeing options in the -# colour scheme: -# search_thread_<component>_<id>_[focus_][fg|bg] -# Note that the sequence of the list defines the search order. The first -# specified query that matches selects the themeing. -rules = { "tag:unread AND tag:flagged":"isunread+flagged", - "tag:unread":"isunread", - "tag:flagged":"isflagged" } - -# comma separated list of the components of a thread line you want highlighted -# if a query matches. -# Possible components are [date|mailcount|tags|authors|subject|content]. -components = subject - -[256c-theme] - -# default formating for tagstrings -# add 'tag_X_[focus_]fg' and 'tag_X_[focus_]bg' to your config to specify -# formating for tagstring 'X' -tag_bg = default -tag_fg = brown -tag_focus_bg = #68a -tag_focus_fg = #ffa - -#draft tag in red -tag_draft_bg = #d66 -tag_draft_fg = white - -# formating of the `help bindings` overlay -help_text_bg = g35 -help_text_fg = default -help_section_bg = g35 -help_section_fg = bold,underline -help_title_bg = g35 -help_title_fg = white,bold,underline - -# attributes used in all modi -global_footer_bg = #006 -global_footer_fg = white -global_notify_error_bg = dark red -global_notify_error_fg = white -global_notify_normal_bg = #68a -global_notify_normal_fg = light gray -global_prompt_bg = g10 -global_prompt_fg = light gray - -# mode specific attributes -bufferlist_focus_bg = #68a -bufferlist_focus_fg = #ffa -bufferlist_results_even_bg = g3 -bufferlist_results_even_fg = default -bufferlist_results_odd_bg = default -bufferlist_results_odd_fg = default -search_thread_authors_bg = default -search_thread_authors_fg = #6d6 -search_thread_authors_isunread_fg = #6d6,bold -search_thread_authors_isunread+flagged_fg = #6d6,bold -search_thread_authors_focus_bg = #68a -search_thread_authors_focus_fg = #8f6 -search_thread_authors_isunread_focus_fg = #8f6,bold -search_thread_authors_isunread+flagged_focus_fg = #8f6,bold -search_thread_bg = default -search_thread_content_bg = default -search_thread_content_fg = #866 -search_thread_content_isunread_fg = #866,bold -search_thread_content_isunread+flagged_fg = #866,bold -search_thread_content_focus_bg = #68a -search_thread_content_focus_fg = #866 -search_thread_content_isunread_focus_fg = #866,bold -search_thread_content_isunread+flagged_focus_fg = #866,bold -search_thread_date_bg = default -search_thread_date_fg = g58 -search_thread_date_isunread_fg = g58,bold -search_thread_date_isflagged_fg = light red -search_thread_date_isunread+flagged_fg = light red,bold -search_thread_date_focus_bg = #68a -search_thread_date_focus_fg = g89 -search_thread_date_isunread_focus_fg = g89,bold -search_thread_date_isflagged_focus_fg = light red -search_thread_date_isunread+flagged_focus_fg = light red,bold -search_thread_fg = default -search_thread_focus_bg = #68a -search_thread_focus_fg = white -search_thread_mailcount_bg = default -search_thread_mailcount_fg = light gray -search_thread_mailcount_isunread_fg = light gray,bold -search_thread_mailcount_isflagged_fg = light red -search_thread_mailcount_isunread+flagged_fg = light red,bold -search_thread_mailcount_focus_bg = #68a -search_thread_mailcount_focus_fg = g89 -search_thread_mailcount_isunread_focus_fg = g89,bold -search_thread_mailcount_isflagged_focus_fg = light red -search_thread_mailcount_isunread+flagged_focus_fg = light red,bold -search_thread_subject_bg = default -search_thread_subject_fg = g58 -search_thread_subject_isunread_fg = g58,bold -search_thread_subject_isflagged_fg = light red -search_thread_subject_isunread+flagged_fg = light red,bold -search_thread_subject_focus_bg = #68a -search_thread_subject_focus_fg = g89 -search_thread_subject_isunread_focus_fg = g89,bold -search_thread_subject_isflagged_focus_fg = light red -search_thread_subject_isunread+flagged_focus_fg = light red,bold -search_thread_tags_bg = default -search_thread_tags_fg = #a86 -search_thread_tags_focus_bg = #68a -search_thread_tags_focus_fg = #ff8 -thread_attachment_bg = dark gray -thread_attachment_fg = light gray -thread_attachment_focus_bg = light green -thread_attachment_focus_fg = light gray -thread_body_bg = default -thread_body_fg = light gray -thread_header_bg = dark gray -thread_header_fg = white -thread_header_key_bg = dark gray -thread_header_key_fg = white -thread_header_value_bg = dark gray -thread_header_value_fg = light gray -thread_summary_even_bg = #068 -thread_summary_even_fg = white -thread_summary_focus_bg = g58 -thread_summary_focus_fg = #ff8 -thread_summary_odd_bg = #006 -thread_summary_odd_fg = white - - -[16c-theme] -global_footer_bg = dark blue -global_footer_fg = light green -global_notify_error_bg = dark red -global_notify_error_fg = white -global_notify_normal_bg = dark gray -global_notify_normal_fg = light gray -global_prompt_bg = black -global_prompt_fg = light gray -help_text_bg = dark gray -help_text_fg = default -help_section_bg = dark gray -help_section_fg = bold,underline -help_title_bg = dark blue -help_title_fg = white -tag_bg = black -tag_fg = brown -tag_focus_bg = dark gray -tag_focus_fg = white -tag_draft_bg = light red -tag_draft_fg = white -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 -thread_attachment_bg = dark gray -thread_attachment_fg = light gray -thread_attachment_focus_bg = light green -thread_attachment_focus_fg = light gray -thread_body_bg = default -thread_body_fg = light gray -thread_header_bg = dark gray -thread_header_fg = white -thread_header_key_bg = dark gray -thread_header_key_fg = white -thread_header_value_bg = dark gray -thread_header_value_fg = light gray -thread_summary_even_bg = light blue -thread_summary_even_fg = white -thread_summary_focus_bg = dark cyan -thread_summary_focus_fg = white -thread_summary_odd_bg = dark blue -thread_summary_odd_fg = white -search_thread_authors_bg = default -search_thread_authors_fg = dark green -search_thread_authors_isunread_fg = dark green,bold -search_thread_authors_focus_bg = dark gray -search_thread_authors_focus_fg = dark green,bold -search_thread_bg = default -search_thread_content_bg = default -search_thread_content_fg = dark gray -search_thread_content_focus_bg = dark gray -search_thread_content_focus_fg = black -search_thread_date_bg = default -search_thread_date_fg = light gray -search_thread_date_focus_bg = dark gray -search_thread_date_focus_fg = light gray -search_thread_fg = default -search_thread_focus_bg = dark gray -search_thread_focus_fg = light gray -search_thread_mailcount_bg = default -search_thread_mailcount_fg = light gray -search_thread_mailcount_focus_bg = dark gray -search_thread_mailcount_focus_fg = light gray -search_thread_subject_bg = default -search_thread_subject_fg = light gray -search_thread_subject_isunread_fg = light gray,bold -search_thread_subject_isflagged_fg = light red -search_thread_subject_focus_bg = dark gray -search_thread_subject_focus_fg = light gray -search_thread_subject_isunread_focus_fg = light gray,bold -search_thread_subject_isflagged_focus_fg = light red,bold -search_thread_tags_bg = default -search_thread_tags_fg = brown -search_thread_tags_focus_bg = dark gray -search_thread_tags_focus_fg = yellow,bold - - -[1c-theme] -global_footer = standout -global_notify_error = standout -global_notify_normal = default -global_prompt = -help_text = default -help_section = underline -help_title = standout -tag = default -tag_focus = standout, bold -tag_draft = standout -bufferlist_focus = standout -bufferlist_results_even = default -bufferlist_results_odd = default -search_thread = default -search_thread_authors = default,underline -search_thread_authors_focus = standout -search_thread_content = default -search_thread_content_focus = standout -search_thread_date = default -search_thread_date_focus = standout -search_thread_focus = standout -search_thread_mailcount = default -search_thread_mailcount_focus = standout -search_thread_subject = default -search_thread_subject_isunread = bold -search_thread_subject_isflagged = underline -search_thread_subject_focus = standout -search_thread_subject_isunread_focus = standout,bold -search_thread_subject_isflagged_focus = standout,underline -search_thread_tags = bold -search_thread_tags_focus = standout -thread_attachment = default -thread_attachment_focus = underline -thread_body = default -thread_header = default -thread_header_key = default -thread_header_value = default -thread_summary_even = -thread_summary_focus = standout -thread_summary_odd = diff --git a/alot/defaults/alot.rc.new b/alot/defaults/alot.rc.new new file mode 100644 index 00000000..099d730f --- /dev/null +++ b/alot/defaults/alot.rc.new @@ -0,0 +1,10 @@ +[tags] +[[flagged]] + fg = light red + translated = ⚑ +[[unread]] + translated = ✉ +[[replied]] + translated = ⏎ +[[inbox]] + translated = ⊡ diff --git a/alot/defaults/alot.rc.spec b/alot/defaults/alot.rc.spec new file mode 100644 index 00000000..296d58f0 --- /dev/null +++ b/alot/defaults/alot.rc.spec @@ -0,0 +1,162 @@ +ask_subject = boolean(default=True) # ask for subject when compose + +# confirm exit +bug_on_exit = boolean(default=False) + +# offset of next focussed buffer if the current one gets closed +bufferclose_focus_offset = integer(default=-1) + +# number of colours your terminal supports +colourmode = option(1, 16, 256, default=256) + +# number of spaces used to replace tab characters +tabwidth = integer(default=8) + +# templates directory that contains your message templates. +# It will be used if you give `compose --template` a filename without a path prefix. +template_dir = string(default='$XDG_CONFIG_HOME/alot/templates') + +# directory containing theme files +themes_dir = string(default=None) + +# name of the theme to use +theme = string(default=None) + +# fill threadline with message content +display_content_in_threadline = boolean(default=False) + +# headers that get displayed by default +displayed_headers = string_list(default=list(From,To,Cc,Bcc,Subject)) + +# headers that are hidden in envelope buffers by default +envelope_headers_blacklist = string_list(default=list(In-Reply-To,References)) + +# set terminal command used for spawning shell commands +terminal_cmd = string(default='x-terminal-emulator -e') + +# editor command +# if unset, alot will first try the EDITOR env variable, then /usr/bin/editor +editor_cmd = string(default=None) + +# file encoding used by your editor +editor_writes_encoding = string(default='UTF-8') + +# use terminal_command to spawn a new terminal for the editor? +editor_spawn = boolean(default=False) + +# call editor in separate thread. +# In case your editor doesn't run in the same window as alot, setting true here +# will make alot non-blocking during edits +editor_in_thread = boolean(default=False) + +# Which header fields should be editable in your editor +# used are those that match the whitelist and don't macht the blacklist. +# in both cases '*' may be used to indicate all fields. +edit_headers_whitelist = string_list(default=list(*,)) +edit_headers_blacklist = string_list(default=list(Content-Type,MIME-Version,References,In-Reply-To)) + +# timeout in secs after a failed attempt to flush is repeated +flush_retry_timeout = integer(default=5) + +# where to look up hooks +hooksfile = string(default='~/.config/alot/hooks.py') + +# time in secs to display status messages +notify_timeout = integer(default=2) + +# display statusline? +show_statusbar = boolean(default=True) + +# timestamp format in strftime format syntax: +# http://docs.python.org/library/datetime.html#strftime-strptime-behavior +timestamp_format = string(default=None) + +# max length of authors line in thread widgets +authors_maxlength = integer(default=30) + +# 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 = string(default=None) + +# initial command when none is given as argument: +initial_command = string(default='search tag:inbox AND NOT tag:killed') + +# default sort order of results in a search +search_threads_sort_order = option('oldest_first', 'newest_first', 'message_id', 'unsorted', default='newest_first') + +# in case more than one account has an address book: +# 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 = boolean(default=False) + +# shut down when the last buffer gets closed +quit_on_last_bclose = boolean(default=False) + +# value of the User-Agent header used for outgoing mails. +# setting this to the empty string will cause alot to omit the header all together. +# The string '$VERSION' will be replaced by the version string of the running instance. +user_agent = string(default='alot/$VERSION') + +# Keybindings +[bindings] + __many__ = string(default=None) + [[___many___]] + __many__ = string(default=None) + +[tags] + # for each tag + [[__many__]] + # foreground + fg = string(default=None) + # background + bg = string(default=None) + # foreground if focussed + focus_fg = string(default=None) + # background if focussed + focus_bg = string(default=None) + # alternative string representation + translated = string(default=None) + +[accounts] +[[__many__]] + # your email address + address = string + + # used to format the (proposed) From-header in outgoing mails + realname = string + + # used to clear your addresses/ match account when formating replies + aliases = string_list(default=list()) + + # how to send mails + sendmail_command = string(default='sendmail') + + # specifies the mailbox where you want outgoing mails to be stored after successfully sending them, e.g. + # where to store outgoing mail, e.g. `maildir:///home/you/mail//Sent` + # You can use mbox, maildir, mh, babyl and mmdf in the protocol part of the url. + sent_box = string(default=None) + + # how to tag sent mails. + sent_tags = string_list(default=list('sent')) + + # path to signature file that gets attached to all outgoing mails from this account, optionally + # renamed to `signature_filename`. + signature = string(default=None) + + # attach signature file if set to True, append its content (mimetype text) + # to the body text if set to False. Defaults to False. + signature_as_attachment = boolean(default=False) + + # signature file's name as it appears in outgoing mails if + # signature_as_attachment is set to True + signature_filename = string(default=None) + + # command to lookup contacts. + # 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. + abook_command = string(default=None) + + # The regular expression used to match name/address pairs in the output of `abook_command` + abook_regexp = string(default=None) diff --git a/alot/defaults/bindings b/alot/defaults/bindings new file mode 100644 index 00000000..50ac0d32 --- /dev/null +++ b/alot/defaults/bindings @@ -0,0 +1,65 @@ +j = move down +k = move up +' ' = move page down +esc = cancel +enter = select +@ = refresh +? = help bindings +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] + x = close + select = openfocussed + +[search] + a = toggletags inbox + & = toggletags killed + ! = toggletags flagged + s = toggletags unread + l = retagprompt + O = refineprompt + | = refineprompt + +[envelope] + a = prompt "attach ~/" + y = send + P = save + s = 'refine Subject' + t = 'refine To' + b = 'refine Bcc' + c = 'refine Cc' + select = edit + H = toggleheaders + +[taglist] + +[thread] + C = fold --all + E = unfold --all + c = fold + e = unfold + < = fold + > = unfold + H = toggleheaders + h = togglesource + P = print --all --separately --add_tags + S = save --all + g = reply --all + f = forward + p = print --add_tags + n = editnew + s = save + r = reply + | = prompt 'pipeto ' diff --git a/alot/defaults/default.theme b/alot/defaults/default.theme new file mode 100644 index 00000000..269bf856 --- /dev/null +++ b/alot/defaults/default.theme @@ -0,0 +1,317 @@ +[1] +[[global]] + [[[footer]]] + fg = 'standout' + [[[notify_error]]] + fg = 'standout' + [[[notify_normal]]] + fg = 'default' + [[[prompt]]] + fg = 'default' + [[[tag]]] + fg = 'default' + [[[tag_focus]]] + fg = 'standout, bold' + [[[tag_draft]]] + fg = 'standout' +[[help]] + [[[text]]] + fg = 'default' + [[[section]]] + fg = 'underline' + [[[title]]] + fg = 'standout' +[[bufferlist]] + [[[focus]]] + fg = 'standout' + [[[results_even]]] + fg = 'default' + [[[results_odd]]] + fg = 'default' +[[search]] + [[[thread]]] + fg = 'default' + [[[thread_authors]]] + fg = 'default,underline' + [[[thread_authors_focus]]] + fg = 'standout' + [[[thread_content]]] + fg = 'default' + [[[thread_content_focus]]] + fg = 'standout' + [[[thread_date]]] + fg = 'default' + [[[thread_date_focus]]] + fg = 'standout' + [[[thread_focus]]] + fg = 'standout' + [[[thread_mailcount]]] + fg = 'default' + [[[thread_mailcount_focus]]] + fg = 'standout' + [[[thread_subject]]] + fg = 'default' + [[[thread_subject_isunread]]] + fg = 'bold' + [[[thread_subject_isflagged]]] + fg = 'underline' + [[[thread_subject_focus]]] + fg = 'standout' + [[[thread_subject_isunread_focus]]] + fg = 'standout,bold' + [[[thread_subject_isflagged_focus]]] + fg = 'standout,underline' + [[[thread_tags]]] + fg = 'bold' + [[[thread_tags_focus]]] + fg = 'standout' +[[thread]] + [[[attachment]]] + fg = 'default' + [[[attachment_focus]]] + fg = 'underline' + [[[body]]] + fg = 'default' + [[[header]]] + fg = 'default' + [[[header_key]]] + fg = 'default' + [[[header_value]]] + fg = 'default' + [[[summary_even]]] + fg = 'default' + [[[summary_focus]]] + fg = 'standout' + [[[summary_odd]]] + fg = 'default' + +[16] +[[global]] + [[[footer]]] + bg = 'dark blue' + fg = 'light green' + [[[notify_error]]] + bg = 'dark red' + fg = 'white' + [[[notify_normal]]] + bg = 'dark gray' + fg = 'light gray' + [[[prompt]]] + bg = 'black' + fg = 'light gray' + [[[tag]]] + bg = 'black' + fg = 'brown' + [[[tag_focus]]] + bg = 'dark gray' + fg = 'white' + [[[tag_draft]]] + bg = 'light red' + fg = 'white' +[[help]] + [[[text]]] + bg = 'dark gray' + fg = 'default' + [[[section]]] + bg = 'dark gray' + fg = 'bold,underline' + [[[title]]] + bg = 'dark blue' + fg = 'white' +[[bufferlist]] + [[[focus]]] + bg = 'dark gray' + fg = 'white' + [[[results_even]]] + bg = 'black' + fg = 'light gray' + [[[results_odd]]] + bg = 'black' + fg = 'light gray' +[[thread]] + [[[attachment]]] + bg = 'dark gray' + fg = 'light gray' + [[[attachment_focus]]] + bg = 'light green' + fg = 'light gray' + [[[body]]] + bg = 'default' + fg = 'light gray' + [[[header]]] + bg = 'dark gray' + fg = 'white' + [[[header_key]]] + bg = 'dark gray' + fg = 'white' + [[[header_value]]] + bg = 'dark gray' + fg = 'light gray' + [[[summary_even]]] + bg = 'light blue' + fg = 'white' + [[[summary_focus]]] + bg = 'dark cyan' + fg = 'white' + [[[summary_odd]]] + bg = 'dark blue' + fg = 'white' +[[search]] + [[[thread]]] + bg = 'default' + fg = 'default' + [[[thread_authors]]] + bg = 'default' + fg = 'dark green' + [[[thread_authors_focus]]] + bg = 'dark gray' + fg = 'dark green,bold' + [[[thread_content]]] + bg = 'default' + fg = 'dark gray' + [[[thread_content_focus]]] + bg = 'dark gray' + fg = 'black' + [[[thread_date]]] + bg = 'default' + fg = 'light gray' + [[[thread_date_focus]]] + bg = 'dark gray' + fg = 'light gray' + [[[thread_focus]]] + bg = 'dark gray' + fg = 'light gray' + [[[thread_mailcount]]] + bg = 'default' + fg = 'light gray' + [[[thread_mailcount_focus]]] + bg = 'dark gray' + fg = 'light gray' + [[[thread_subject]]] + bg = 'default' + fg = 'light gray' + [[[thread_subject_focus]]] + bg = 'dark gray' + fg = 'light gray' + [[[thread_tags]]] + bg = 'default' + fg = 'brown' + [[[thread_tags_focus]]] + bg = 'dark gray' + fg = 'yellow,bold' + +[256] +[[global]] + # attributes used in all modi + [[[footer]]] + bg = '#006' + fg = 'white' + [[[notify_error]]] + bg = 'dark red' + fg = 'white' + [[[notify_normal]]] + bg = '#68a' + fg = 'light gray' + [[[prompt]]] + bg = 'g10' + fg = 'light gray' + [[[tag]]] + bg = 'default' + fg = 'brown' + [[[tag_focus]]] + bg = '#68a' + fg = '#ffa' +[[help]] + # formating of the `help bindings` overlay + [[[text]]] + bg = 'g35' + fg = 'default' + [[[section]]] + bg = 'g35' + fg = 'bold,underline' + [[[title]]] + bg = 'g35' + fg = 'white,bold,underline' +# mode specific attributes +[[bufferlist]] + [[[focus]]] + bg = '#68a' + fg = '#ffa' + [[[results_even]]] + bg = 'g3' + fg = 'default' + [[[results_odd]]] + bg = 'default' + fg = 'default' +[[search]] + [[[thread]]] + bg = 'default' + fg = '#6d6' + + [[[thread_authors]]] + bg = 'default' + fg = '#6d6' + [[[thread_authors_focus]]] + bg = '#68a' + fg = '#8f6' + [[[thread_content]]] + bg = 'default' + fg = '#866' + [[[thread_content_focus]]] + bg = '#68a' + fg = '#866' + [[[thread_date]]] + bg = 'default' + fg = 'g58' + [[[thread_date_focus]]] + bg = '#68a' + fg = 'g89' + [[[thread_focus]]] + bg = '#68a' + fg = 'white' + [[[thread_mailcount]]] + bg = 'default' + fg = 'light gray' + [[[thread_mailcount_focus]]] + bg = '#68a' + fg = 'g89' + [[[thread_subject]]] + bg = 'default' + fg = 'g58' + [[[thread_subject_focus]]] + bg = '#68a' + fg = 'g89' + [[[thread_tags]]] + bg = 'default' + fg = '#a86' + [[[thread_tags_focus]]] + bg = '#68a' + fg = '#ff8' +[[thread]] + [[[attachment]]] + bg = 'dark gray' + fg = 'light gray' + [[[attachment_focus]]] + bg = 'light green' + fg = 'light gray' + [[[body]]] + bg = 'default' + fg = 'light gray' + [[[header]]] + bg = 'dark gray' + fg = 'white' + [[[header_key]]] + bg = 'dark gray' + fg = 'white' + [[[header_value]]] + bg = 'dark gray' + fg = 'light gray' + [[[summary_even]]] + bg = '#068' + fg = 'white' + [[[summary_focus]]] + bg = 'g58' + fg = '#ff8' + [[[summary_odd]]] + bg = '#006' + fg = 'white' diff --git a/alot/defaults/notmuch.rc.spec b/alot/defaults/notmuch.rc.spec new file mode 100644 index 00000000..fb94ffd0 --- /dev/null +++ b/alot/defaults/notmuch.rc.spec @@ -0,0 +1,3 @@ +[maildir] +synchronize_flags = boolean(default=False) + diff --git a/alot/defaults/theme.spec b/alot/defaults/theme.spec new file mode 100644 index 00000000..d1ea47ab --- /dev/null +++ b/alot/defaults/theme.spec @@ -0,0 +1,317 @@ +[1] +[[global]] + [[[footer]]] + fg = string(default='default') + [[[notify_error]]] + fg = string(default='default') + [[[notify_normal]]] + fg = string(default='default') + [[[prompt]]] + fg = string(default='default') + [[[tag]]] + fg = string(default='default') + [[[tag_focus]]] + fg = string(default='default') + [[[tag_draft]]] + fg = string(default='default') +[[help]] + [[[text]]] + fg = string(default='default') + [[[section]]] + fg = string(default='default') + [[[title]]] + fg = string(default='default') +[[bufferlist]] + [[[focus]]] + fg = string(default='default') + [[[results_even]]] + fg = string(default='default') + [[[results_odd]]] + fg = string(default='default') +[[search]] + [[[thread]]] + fg = string(default='default') + [[[thread_authors]]] + fg = string(default='default') + [[[thread_authors_focus]]] + fg = string(default='default') + [[[thread_content]]] + fg = string(default='default') + [[[thread_content_focus]]] + fg = string(default='default') + [[[thread_date]]] + fg = string(default='default') + [[[thread_date_focus]]] + fg = string(default='default') + [[[thread_focus]]] + fg = string(default='default') + [[[thread_mailcount]]] + fg = string(default='default') + [[[thread_mailcount_focus]]] + fg = string(default='default') + [[[thread_subject]]] + fg = string(default='default') + [[[thread_subject_isunread]]] + fg = string(default='default') + [[[thread_subject_isflagged]]] + fg = string(default='default') + [[[thread_subject_focus]]] + fg = string(default='default') + [[[thread_subject_isunread_focus]]] + fg = string(default='default') + [[[thread_subject_isflagged_focus]]] + fg = string(default='default') + [[[thread_tags]]] + fg = string(default='default') + [[[thread_tags_focus]]] + fg = string(default='default') +[[thread]] + [[[attachment]]] + fg = string(default='default') + [[[attachment_focus]]] + fg = string(default='default') + [[[body]]] + fg = string(default='default') + [[[header]]] + fg = string(default='default') + [[[header_key]]] + fg = string(default='default') + [[[header_value]]] + fg = string(default='default') + [[[summary_even]]] + fg = string(default='default') + [[[summary_focus]]] + fg = string(default='default') + [[[summary_odd]]] + fg = string(default='default') + +[16] +[[global]] + [[[footer]]] + bg = string(default='default') + fg = string(default='default') + [[[notify_error]]] + bg = string(default='default') + fg = string(default='default') + [[[notify_normal]]] + bg = string(default='default') + fg = string(default='default') + [[[prompt]]] + bg = string(default='default') + fg = string(default='default') + [[[tag]]] + bg = string(default='default') + fg = string(default='default') + [[[tag_focus]]] + bg = string(default='default') + fg = string(default='default') + [[[tag_draft]]] + bg = string(default='default') + fg = string(default='default') +[[help]] + [[[text]]] + bg = string(default='default') + fg = string(default='default') + [[[section]]] + bg = string(default='default') + fg = string(default='default') + [[[title]]] + bg = string(default='default') + fg = string(default='default') +[[bufferlist]] + [[[focus]]] + bg = string(default='default') + fg = string(default='default') + [[[results_even]]] + bg = string(default='default') + fg = string(default='default') + [[[results_odd]]] + bg = string(default='default') + fg = string(default='default') +[[thread]] + [[[attachment]]] + bg = string(default='default') + fg = string(default='default') + [[[attachment_focus]]] + bg = string(default='default') + fg = string(default='default') + [[[body]]] + bg = string(default='default') + fg = string(default='default') + [[[header]]] + bg = string(default='default') + fg = string(default='default') + [[[header_key]]] + bg = string(default='default') + fg = string(default='default') + [[[header_value]]] + bg = string(default='default') + fg = string(default='default') + [[[summary_even]]] + bg = string(default='default') + fg = string(default='default') + [[[summary_focus]]] + bg = string(default='default') + fg = string(default='default') + [[[summary_odd]]] + bg = string(default='default') + fg = string(default='default') +[[search]] + [[[thread]]] + bg = string(default='default') + fg = string(default='default') + [[[thread_focus]]] + bg = string(default='default') + fg = string(default='default') + [[[thread_authors]]] + bg = string(default='default') + fg = string(default='default') + [[[thread_authors_focus]]] + bg = string(default='default') + fg = string(default='default') + [[[thread_content]]] + bg = string(default='default') + fg = string(default='default') + [[[thread_content_focus]]] + bg = string(default='default') + fg = string(default='default') + [[[thread_date]]] + bg = string(default='default') + fg = string(default='default') + [[[thread_date_focus]]] + bg = string(default='default') + fg = string(default='default') + [[[thread_mailcount]]] + bg = string(default='default') + fg = string(default='default') + [[[thread_mailcount_focus]]] + bg = string(default='default') + fg = string(default='default') + [[[thread_subject]]] + bg = string(default='default') + fg = string(default='default') + [[[thread_subject_focus]]] + bg = string(default='default') + fg = string(default='default') + [[[thread_tags]]] + bg = string(default='default') + fg = string(default='default') + [[[thread_tags_focus]]] + bg = string(default='default') + fg = string(default='default') + +[256] +[[global]] + # attributes used in all modi + [[[footer]]] + bg = string(default=None) + fg = string(default=None) + [[[notify_error]]] + bg = string(default=None) + fg = string(default=None) + [[[notify_normal]]] + bg = string(default=None) + fg = string(default=None) + [[[prompt]]] + bg = string(default=None) + fg = string(default=None) + [[[tag]]] + bg = string(default=None) + fg = string(default=None) + [[[tag_focus]]] + bg = string(default=None) + fg = string(default=None) +[[help]] + # formating of the `help bindings` overlay + [[[text]]] + bg = string(default=None) + fg = string(default=None) + [[[section]]] + bg = string(default=None) + fg = string(default=None) + [[[title]]] + bg = string(default=None) + fg = string(default=None) +# mode specific attributes +[[bufferlist]] + [[[focus]]] + bg = string(default=None) + fg = string(default=None) + [[[results_even]]] + bg = string(default=None) + fg = string(default=None) + [[[results_odd]]] + bg = string(default=None) + fg = string(default=None) +[[search]] + [[[thread]]] + fg = string(default=None) + [[[thread_authors]]] + bg = string(default=None) + fg = string(default=None) + [[[thread_authors_focus]]] + bg = string(default=None) + fg = string(default=None) + [[[thread_content]]] + bg = string(default=None) + fg = string(default=None) + [[[thread_content_focus]]] + bg = string(default=None) + fg = string(default=None) + [[[thread_date]]] + bg = string(default=None) + fg = string(default=None) + [[[thread_date_focus]]] + bg = string(default=None) + fg = string(default=None) + [[[thread_focus]]] + bg = string(default=None) + fg = string(default=None) + [[[thread_mailcount]]] + bg = string(default=None) + fg = string(default=None) + [[[thread_mailcount_focus]]] + bg = string(default=None) + fg = string(default=None) + [[[thread_subject]]] + bg = string(default=None) + fg = string(default=None) + [[[thread_subject_focus]]] + bg = string(default=None) + fg = string(default=None) + [[[thread_tags]]] + bg = string(default=None) + fg = string(default=None) + [[[thread_tags_focus]]] + bg = string(default=None) + fg = string(default=None) +[[thread]] + [[[attachment]]] + bg = string(default=None) + fg = string(default=None) + [[[attachment_focus]]] + bg = string(default=None) + fg = string(default=None) + [[[body]]] + bg = string(default=None) + fg = string(default=None) + [[[header]]] + bg = string(default=None) + fg = string(default=None) + [[[header_key]]] + bg = string(default=None) + fg = string(default=None) + [[[header_value]]] + bg = string(default=None) + fg = string(default=None) + [[[summary_even]]] + bg = string(default=None) + fg = string(default=None) + [[[summary_focus]]] + bg = string(default=None) + fg = string(default=None) + [[[summary_odd]]] + bg = string(default=None) + fg = string(default=None) + + diff --git a/alot/helper.py b/alot/helper.py index cd258c7a..91aae324 100644 --- a/alot/helper.py +++ b/alot/helper.py @@ -1,3 +1,4 @@ +# coding=utf-8 from datetime import date from datetime import timedelta from collections import deque @@ -17,8 +18,6 @@ from twisted.internet.defer import Deferred import StringIO import logging -from settings import config - def safely_get(clb, E, on_error=''): """ @@ -38,7 +37,7 @@ def safely_get(clb, E, on_error=''): return on_error -def string_sanitize(string, tab_width=None): +def string_sanitize(string, tab_width=8): r""" strips, and replaces non-printable characters @@ -53,8 +52,6 @@ def string_sanitize(string, tab_width=None): >>> string_sanitize('foo\t\tbar', 8) 'foo bar' """ - if tab_width == None: - tab_width = config.getint('general', 'tabwidth') string = string.strip() string = string.replace('\r', '') diff --git a/alot/init.py b/alot/init.py index e602595b..b3bba246 100755 --- a/alot/init.py +++ b/alot/init.py @@ -3,9 +3,8 @@ import sys import logging import os -import settings +from settings import settings, ConfigError import ConfigParser -from account import AccountManager from db import DBManager from ui import UI import alot.commands as commands @@ -114,6 +113,17 @@ def main(): print '%s: Try --help for usage details.' % (sys.argv[0]) sys.exit(1) + # logging + root_logger = logging.getLogger() + for log_handler in root_logger.handlers: + root_logger.removeHandler(log_handler) + root_logger = None + numeric_loglevel = getattr(logging, args['debug-level'].upper(), None) + logfilename = os.path.expanduser(args['logfile']) + logformat = '%(levelname)s:%(module)s:%(message)s' + logging.basicConfig(level=numeric_loglevel, filename=logfilename, + format=logformat) + # locate alot config files configfiles = [ os.path.join(os.environ.get('XDG_CONFIG_HOME', @@ -128,42 +138,29 @@ def main(): configfiles.insert(0, expanded_path) # locate notmuch config - notmuchfile = os.path.expanduser(args['notmuch-config']) - - try: - # read the first alot config file we find - for configfilename in configfiles: - if os.path.exists(configfilename): - settings.config.read(configfilename) - break # use only the first + notmuchconfig = os.path.expanduser(args['notmuch-config']) - # read notmuch config - settings.notmuchconfig.read(notmuchfile) + alotconfig = None + # read the first alot config file we find + for configfilename in configfiles: + if os.path.exists(configfilename): + alotconfig = configfilename + break # use only the first - except ConfigParser.Error, e: # exit on parse errors + try: + settings.read_config(alotconfig) + settings.read_notmuch_config(notmuchconfig) + except ConfigError, e: # exit on parse errors sys.exit(e) - # logging - ## reset - root_logger = logging.getLogger() - for log_handler in root_logger.handlers: - root_logger.removeHandler(log_handler) - root_logger = None - ## setup - numeric_loglevel = getattr(logging, args['debug-level'].upper(), None) - logfilename = os.path.expanduser(args['logfile']) - logformat = '%(levelname)s:%(module)s:%(message)s' - logging.basicConfig(level=numeric_loglevel, filename=logfilename, - format=logformat) + # store options given by config swiches to the settingsManager: + if args['colour-mode']: + settings.set('colourmode', args['colour-mode']) - #logging.debug(commands.COMMANDS) # get ourselves a database manager dbman = DBManager(path=args['mailindex-path'], ro=args['read-only']) - #accountman - aman = AccountManager(settings.config) - - # get initial searchstring + # determine what to do try: if args.subCommand == 'search': query = ' '.join(args.subOptions.args) @@ -174,18 +171,13 @@ def main(): cmdstring = 'compose %s' % args.subOptions.as_argparse_opts() cmd = commands.commandfactory(cmdstring, 'global') else: - default_commandline = settings.config.get('general', - 'initial_command') + default_commandline = settings.get('initial_command') cmd = commands.commandfactory(default_commandline, 'global') except CommandParseError, e: sys.exit(e) # set up and start interface - UI(dbman, - aman, - cmd, - args['colour-mode'], - ) + UI(dbman, cmd) if __name__ == "__main__": main() diff --git a/alot/message.py b/alot/message.py index 61a49804..4feb9a62 100644 --- a/alot/message.py +++ b/alot/message.py @@ -15,8 +15,7 @@ from notmuch import NullPointerError from alot import __version__ import logging import helper -from settings import get_mime_handler -from settings import config +from settings import settings from helper import string_sanitize from helper import string_decode @@ -135,11 +134,11 @@ class Message(object): """ if self._datetime == None: return None - if config.has_option('general', 'timestamp_format'): - formatstring = config.get('general', 'timestamp_format') - res = self._datetime.strftime(formatstring) - else: + formatstring = settings.get('timestamp_format') + if formatstring == None: res = helper.pretty_datetime(self._datetime) + else: + res = self._datetime.strftime(formatstring) return res def get_author(self): @@ -313,8 +312,8 @@ def extract_body(mail, types=None): body_parts.append(string_sanitize(raw_payload)) else: #get mime handler - handler = get_mime_handler(ctype, key='view', - interactive=False) + handler = settings.get_mime_handler(ctype, key='view', + interactive=False) if handler: #open tempfile. Not all handlers accept stuff from stdin tmpfile = tempfile.NamedTemporaryFile(delete=False, @@ -597,7 +596,7 @@ class Envelope(object): if 'User-Agent' in headers: uastring_format = headers['User-Agent'][0] else: - uastring_format = config.get('general', 'user_agent').strip() + uastring_format = settings.get('user_agent').strip() uastring = uastring_format % {'version': __version__} if uastring: headers['User-Agent'] = [uastring] diff --git a/alot/settings.py b/alot/settings.py index d88b5be0..b95595d7 100644 --- a/alot/settings.py +++ b/alot/settings.py @@ -6,244 +6,316 @@ import json import mailcap import codecs import logging +import urwid +from urwid import AttrSpec, AttrSpecError +from configobj import ConfigObj, ConfigObjError, flatten_errors, Section +from validate import Validator + +from account import SendmailAccount, MatchSdtoutAddressbook from collections import OrderedDict from ConfigParser import SafeConfigParser, ParsingError, NoOptionError +DEFAULTSPATH = os.path.join(os.path.dirname(__file__), 'defaults') -class FallbackConfigParser(SafeConfigParser): - """:class:`~ConfigParser.SafeConfigParser` that allows fallback values""" - def __init__(self): - SafeConfigParser.__init__(self) - self.optionxform = lambda x: x - def get(self, section, option, fallback=None, *args, **kwargs): - """get a config option +class ConfigError(Exception): + pass - :param section: section name - :type section: str - :param option: option key - :type option: str - :param fallback: the value to fall back if option undefined - :type fallback: str - """ - if SafeConfigParser.has_option(self, section, option): - return SafeConfigParser.get(self, section, option, *args, **kwargs) - elif fallback != None: - return fallback - else: - raise NoOptionError(option, section) - def getstringlist(self, section, option, **kwargs): - """directly parses a config value into a list of strings""" - stringlist = list() - if self.has_option(section, option): - value = self.get(section, option, **kwargs) - stringlist = [s.strip() for s in value.split(',') if s.strip()] - return stringlist +def read_config(configpath=None, specpath=None): + """ + get a (validated) config object for given config file path. + :param configpath: path to config-file + :type configpath: str + :param specpath: path to spec-file + :type specpath: str + :rtype: `configobj.ConfigObj` + """ + try: + config = ConfigObj(infile=configpath, configspec=specpath, + file_error=True, encoding='UTF8') + except (ConfigObjError, IOError), e: + raise ConfigError('Could not read "%s": %s' % (configpath, e)) + + if specpath: + validator = Validator() + results = config.validate(validator) + + if results != True: + error_msg = 'Validation errors occurred:\n' + for (section_list, key, _) in flatten_errors(config, results): + if key is not None: + msg = 'key "%s" in section "%s" failed validation' + msg = msg % (key, ', '.join(section_list)) + else: + msg = 'section "%s" is malformed' % ', '.join(section_list) + error_msg += msg + '\n' + raise ConfigError(error_msg) + return config -class AlotConfigParser(FallbackConfigParser): - """:class:`FallbackConfigParser` for alots config.""" - def __init__(self): - FallbackConfigParser.__init__(self) - self.hooks = None - def get_hook(self, key): - """return hook (`callable`) identified by `key`""" - if self.hooks: - if key in self.hooks.__dict__: - return self.hooks.__dict__[key] - return None +class Theme(object): + """Colour theme""" + def __init__(self, path): + """ + :param path: path to theme file + :type path: str + """ + self._spec = os.path.join(DEFAULTSPATH, 'theme.spec') + self._config = read_config(path, self._spec) + self.attributes = self._parse_attributes(self._config) - def read(self, file): - if not os.path.isfile(file): - return - - SafeConfigParser.readfp(self, codecs.open(file, "r", "utf8")) - if self.has_option('general', 'hooksfile'): - hf = os.path.expanduser(self.get('general', 'hooksfile')) - if hf is not None: - try: - self.hooks = imp.load_source('hooks', hf) - except: - logging.debug('unable to load hooks file:%s' % hf) - - # 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 _parse_attributes(self, c): + """ + parse a (previously validated) valid theme file + into urwid AttrSpec attributes for internal use. - # parse highlighting rules - self._highlighting_rules = self._parse_highlighting_rules() + :param c: config object for theme file + :type c: `configobj.ConfigObj` + :raises: `ConfigError` + """ - def get_palette(self): - """parse the sections '1c-theme', '16c-theme' and '256c-theme' - into an urwid compatible colour palette. + attributes = {} + for sec in c.sections: + try: + colours = int(sec) + except ValueError: + err_msg = 'section name %s is not a valid colour mode' + raise ConfigError(err_msg % sec) + attributes[colours] = {} + for mode in c[sec].sections: + attributes[colours][mode] = {} + for themable in c[sec][mode].sections: + block = c[sec][mode][themable] + fg = block['fg'] + if colours == 1: + bg = 'default' + else: + bg = block['bg'] + if colours == 256: + fg = fg or c['16'][mode][themable][fg] + bg = bg or c['16'][mode][themable][bg] + try: + att = AttrSpec(fg, bg, colours) + except AttrSpecError, e: + raise ConfigError(e) + attributes[colours][mode][themable] = att + return attributes + + def get_attribute(self, mode, name, colourmode): + """ + returns requested attribute - :returns: a palette - :rtype: list + :param mode: ui-mode (e.g. `search`,`thread`...) + :type mode: str + :param name: identifier of the atttribute + :type name: str + :param colourmode: colour mode; in [1, 16, 256] + :type colourmode: int """ - mode = self.getint('general', 'colourmode') - ms = "%dc-theme" % mode - names = self.options(ms) - if mode != 1: # truncate '_fg'/'_bg' suffixes for multi-colour themes - names = set([s[:-3] for s in names]) - p = list() - for attr in names: - nf = self._get_theming_option('16c-theme', attr + '_fg') - nb = self._get_theming_option('16c-theme', attr + '_bg') - m = self._get_theming_option('1c-theme', attr) - hf = self._get_theming_option('256c-theme', attr + '_fg') - hb = self._get_theming_option('256c-theme', attr + '_bg') - p.append((attr, nf, nb, m, hf, hb)) - if attr.startswith('tag_') and attr + '_focus' not in names: - nb = self.get('16c-theme', 'tag_focus_bg', - fallback='default') - hb = self.get('256c-theme', 'tag_focus_bg', - fallback='default') - p.append((attr + '_focus', nf, nb, m, hf, hb)) - return p - - def _get_theming_option(self, section, option, default='default'): + return self.attributes[colourmode][mode][name] + + +class SettingsManager(object): + """Organizes user settings""" + def __init__(self, alot_rc=None, notmuch_rc=None, theme=None): """ - Retrieve the value of the given option from the given section of the - config file. - - If the option does not exist, try its parent options before falling - back to the specified default. The parent of an option is the name of - the option itself minus the last section enclosed in underscores; - so the parent of the option `aaa_bbb_ccc_fg` is of the form - `aaa_bbb_fg`. - - :param section: the section of the config file to search for the given - option - :type section: string - :param option: the option to lookup - :type option: string - :param default: the value that is to be returned if neither the - requested option nor a parent exists - :type default: string - :return: the value of the given option, or the specified default - :rtype: string + :param alot_rc: path to alot's config file + :type alot_rc: str + :param notmuch_rc: path to notmuch's config file + :type notmuch_rc: str + :theme: path to initially used theme file + :type theme: str """ - result = '' - if 'focus' in option: - parent_option_re = '(.+)_[^_]+_(focus_(?:fg|bg))' - else: - parent_option_re = '(.+)_[^_]+_(fg|bg)' - - if self.has_option(section, option): - result = self.get(section, option) + self.hooks = None + self._mailcaps = mailcap.getcaps() + + theme_path = theme or os.path.join(DEFAULTSPATH, 'default.theme') + self._theme = Theme(theme_path) + self._bindings = read_config(os.path.join(DEFAULTSPATH, 'bindings')) + + self._config = ConfigObj() + self._accounts = None + self._accountmap = None + self.read_config(alot_rc) + self.read_notmuch_config(notmuch_rc) + + def read_notmuch_config(self, path): + """parse notmuch's config file from path""" + spec = os.path.join(DEFAULTSPATH, 'notmuch.rc.spec') + self._notmuchconfig = read_config(path, spec) + + def read_config(self, path): + """parse alot's config file from path""" + spec = os.path.join(DEFAULTSPATH, 'alot.rc.spec') + newconfig = read_config(path, spec) + self._config.merge(newconfig) + + hooks_path = os.path.expanduser(self._config.get('hooksfile')) + try: + self.hooks = imp.load_source('hooks', hooks_path) + except: + logging.debug('unable to load hooks file:%s' % hooks_path) + if 'bindings' in newconfig: + newbindings = newconfig['bindings'] + if isinstance(newbindings, Section): + self._bindings.merge(newbindings) + # themes + themestring = newconfig['theme'] + themes_dir = self._config.get('themes_dir') + if themes_dir: + themes_dir = os.path.expanduser(themes_dir) else: - has_parent_option = re.search(parent_option_re, option) - if has_parent_option: - parent_option = '{0}_{1}'.format(has_parent_option.group(1), - has_parent_option.group(2)) - result = self._get_theming_option(section, parent_option) + themes_dir = os.path.join(os.environ.get('XDG_CONFIG_HOME', + os.path.expanduser('~/.config')), 'alot', 'themes') + logging.debug(themes_dir) + + if themestring: + if not os.path.isdir(themes_dir): + err_msg = 'cannot find theme %s: themes_dir %s is missing' + raise ConfigError(err_msg % (themestring, themes_dir)) else: - result = default - return result + theme_path = os.path.join(themes_dir, themestring) + self._theme = Theme(theme_path) + + self._accounts = self._parse_accounts(self._config) + self._accountmap = self._account_table(self._accounts) - def _parse_highlighting_rules(self): + def _parse_accounts(self, config): """ - Parse the highlighting rules from the config file. + read accounts information from config - :returns: The highlighting rules - :rtype: :py:class:`collections.OrderedDict` + :param config: valit alot config + :type config: `configobj.ConfigObj` + :returns: list of accounts """ - rules = OrderedDict() - config_string = self.get('highlighting', 'rules') - try: - rules = json.loads(config_string, object_pairs_hook=OrderedDict) - except ValueError as err: - raise ParsingError("Could not parse config option" \ - " 'rules' in section 'highlighting':" \ - " {reason}".format(reason=err)) - return rules - - def get_highlight_rules(self): + accounts = [] + if 'accounts' in config: + for acc in config['accounts'].sections: + accsec = config['accounts'][acc] + args = dict(config['accounts'][acc]) + + if 'abook_command' in accsec: + cmd = accsec['abook_command'] + regexp = accsec['abook_regexp'] + args['abook'] = MatchSdtoutAddressbook(cmd, match=regexp) + del(args['abook_command']) + del(args['abook_regexp']) + + cmd = args['sendmail_command'] + del(args['sendmail_command']) + newacc = SendmailAccount(cmd, **args) + accounts.append(newacc) + return accounts + + def _account_table(self, accounts): + """ + creates a lookup table (emailaddress -> account) for a given list of + accounts + + :param accounts: list of accounts + :type accounts: list of `alot.account.Account` + :returns: hashtable + :rvalue: dict (str -> `alot.account.Account`) + """ + accountmap = {} + for acc in accounts: + accountmap[acc.address] = acc + for alias in acc.aliases: + accountmap[alias] = acc + return accountmap + + def get(self, key): + """ + look up global config values from alot's config + + :param key: key to look up + :type key: str + :returns: config value with type as specified in the spec-file + """ + value = None + if key in self._config: + value = self._config[key] + if isinstance(value, Section): + value = None + return value + + def set(self, key, value): """ - Getter for the highlighting rules. + setter for global config values - :returns: The highlighting rules - :rtype: :py:class:`collections.OrderedDict` + :param key: config option identifise + :type key: str + :param value: option to set + :type value: depends on the specfile :file:`alot.rc.spec` """ - return self._highlighting_rules + self._config[key] = value - def get_tag_theme(self, tag, focus=False, highlight=''): + def get_notmuch_setting(self, section, key): """ - look up attribute string to use for a given tagstring - - :param tag: tagstring to look up - :type tag: str - :param focus: return the 'focussed' attribute - :type focus: bool - :param highlight: suffix of highlight theme - :type highlight: str + look up config values from notmuch's config + + :param section: key is in + :type section: str + :param key: key to look up + :type key: str + :returns: config value with type as specified in the spec-file """ - themes = self._select_tag_themes(tag, focus, highlight) - selected_theme = themes[-1] - for theme in themes: - if self.has_theming(theme): - selected_theme = theme - break - return selected_theme - - def _select_tag_themes(self, tag, focus=False, highlight=''): + value = None + if key in self._notmuchconfig: + value = self._notmuchconfig[key] + return value + + def get_theming_attribute(self, mode, name): """ - Select tag themes based on tag name, focus and highlighting. - - :param tag: the name of the tag to theme - :type tag: str - :param focus: True if this tag is focussed, False otherwise - :type focus: bool - :param highlight: the suffix of the highlighting theme - :type highlight: str - :return: the selected themes, highest priority first - :rtype: list + looks up theming attribute + + :param mode: ui-mode (e.g. `search`,`thread`...) + :type mode: str + :param name: identifier of the atttribute + :type name: str """ - base = 'tag' - themes = [base, base + '_{}'.format(tag)] - if (highlight and - 'tags' in self.getstringlist('highlighting', 'components')): - themes.insert(1, base + '_{}'.format(highlight)) - themes.insert(3, base + '_{}_{}'.format(tag, highlight)) - if focus: - themes = [theme + '_focus' for theme in themes] - themes.reverse() - return themes - - def has_theming(self, theming): + colours = int(self._config.get('colourmode')) + return self._theme.get_attribute(mode, name, colours) + + def get_tagstring_representation(self, tag): """ - Return true if the given theming option exists in the current colour - theme. + looks up user's preferred way to represent a given tagstring - :param theming: The theming option to check for - :type theming: string - :return: True if theming exist, False otherwise - :rtype: bool + This returns a dictionary mapping + 'normal' and 'focussed' to `urwid.AttrSpec` sttributes, + and 'translated' to an alternative string representation """ - mode = self.getint('general', 'colourmode') - theme = '{colours}c-theme'.format(colours=mode) - has_fg = self.has_option(theme, theming + '_fg') - has_bg = self.has_option(theme, theming + '_bg') - return (has_fg or has_bg) + colours = int(self._config.get('colourmode')) + # default attributes: normal and focussed + default = self._theme.get_attribute('global', 'tag', colours) + default_f = self._theme.get_attribute('global', 'tag_focus', colours) + if tag in self._config['tags']: + fg = self._config['tags'][tag]['fg'] or default.foreground + bg = self._config['tags'][tag]['bg'] or default.background + normal = urwid.AttrSpec(fg, bg, colours) + ffg = self._config['tags'][tag]['focus_fg'] or default_f.foreground + fbg = self._config['tags'][tag]['focus_bg'] or default_f.background + focussed = urwid.AttrSpec(ffg, fbg, colours) + translated = self._config['tags'][tag]['translated'] or tag + else: + normal = default + focussed = default_f + translated = tag + + return {'normal': normal, 'focussed': focussed, + 'translated': translated} + + def get_hook(self, key): + """return hook (`callable`) identified by `key`""" + if self.hooks: + if key in self.hooks.__dict__: + return self.hooks.__dict__[key] + return None - def get_mapping(self, mode, key): + def get_keybinding(self, mode, key): """look up keybinding from `MODE-maps` sections :param mode: mode identifier @@ -254,47 +326,83 @@ class AlotConfigParser(FallbackConfigParser): :rtype: str """ cmdline = None - if self.has_option(mode + '-maps', key): - cmdline = self.get(mode + '-maps', key) - elif self.has_option('global-maps', key): - cmdline = self.get('global-maps', key) + bindings = self._bindings + if key in bindings.scalars: + cmdline = bindings[key] + if mode in bindings.sections: + if key in bindings[mode].scalars: + cmdline = bindings[mode][key] return cmdline + def get_accounts(self): + """ + returns known accounts -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')) -mailcaps = mailcap.getcaps() + :rtype: list of :class:`Account` + """ + return self._accounts + def get_account_by_address(self, address): + """ + returns :class:`Account` for a given email address (str) -def get_mime_handler(mime_type, key='view', interactive=True): - """ - get shellcomand defined in the users `mailcap` as handler for files of - given `mime_type`. - - :param mime_type: file type - :type mime_type: str - :param key: identifies one of possibly many commands for this type by - naming the intended usage, e.g. 'edit' or 'view'. Defaults - to 'view'. - :type key: str - :param interactive: choose the "interactive session" handler rather than - the "print to stdout and immediately return" handler - :type interactive: bool - """ - if interactive: - mc_tuple = mailcap.findmatch(mailcaps, - mime_type, - key=key) - else: - mc_tuple = mailcap.findmatch(mailcaps, - mime_type, - key='copiousoutput') - if mc_tuple: - if mc_tuple[1]: - return mc_tuple[1][key] - else: + :param address: address to look up + :type address: string + :rtype: :class:`Account` or None + """ + + for myad in self.get_addresses(): + if myad in address: + return self._accountmap[myad] return None + + def get_main_addresses(self): + """returns addresses of known accounts without its aliases""" + return [a.address for a in self._accounts] + + def get_addresses(self): + """returns addresses of known accounts including all their aliases""" + return self._accountmap.keys() + + def get_addressbooks(self, order=[], append_remaining=True): + """returns list of all defined :class:`AddressBook` objects""" + 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: + abooks.append(a.abook) + return abooks + + def get_mime_handler(self, mime_type, key='view', interactive=True): + """ + get shellcomand defined in the users `mailcap` as handler for files of + given `mime_type`. + + :param mime_type: file type + :type mime_type: str + :param key: identifies one of possibly many commands for this type by + naming the intended usage, e.g. 'edit' or 'view'. Defaults + to 'view'. + :type key: str + :param interactive: choose the "interactive session" handler rather + than the "print to stdout and immediately return" + handler + :type interactive: bool + """ + if interactive: + mc_tuple = mailcap.findmatch(self._mailcaps, mime_type, key=key) + else: + mc_tuple = mailcap.findmatch(self._mailcaps, mime_type, + key='copiousoutput') + if mc_tuple: + if mc_tuple[1]: + return mc_tuple[1][key] + else: + return None + + +settings = SettingsManager() @@ -1,9 +1,8 @@ import urwid import logging from twisted.internet import reactor, defer -from twisted.python.failure import Failure -from settings import config +from settings import settings from buffers import BufferlistBuffer import commands from commands import commandfactory @@ -46,7 +45,7 @@ class InputWrap(urwid.WidgetWrap): mode = self.ui.mode if self.select_cancel_only: mode = 'global' - cmdline = config.get_mapping(mode, key) + cmdline = settings.get_keybinding(mode, key) if cmdline: try: cmd = commandfactory(cmdline, mode) @@ -71,34 +70,28 @@ class UI(object): """points to currently active :class:`~alot.buffers.Buffer`""" dbman = None """Database manager (:class:`~alot.db.DBManager`)""" - accountman = None - """account manager (:class:`~alot.account.AccountManager`)""" - def __init__(self, dbman, accountman, initialcmd, colourmode): + def __init__(self, dbman, initialcmd): """ :param dbman: :class:`~alot.db.DBManager` - :param accountman: :class:`~alot.account.AccountManager` :param initialcmd: commandline applied after setting up interface :type initialcmd: str :param colourmode: determines which theme to chose :type colourmode: int in [1,16,256] """ self.dbman = dbman - self.accountman = accountman - if not colourmode: - colourmode = config.getint('general', 'colourmode') + colourmode = int(settings.get('colourmode')) logging.info('setup gui in %d colours' % colourmode) self.mainframe = urwid.Frame(urwid.SolidFill()) self.inputwrap = InputWrap(self, self.mainframe) self.mainloop = urwid.MainLoop(self.inputwrap, - config.get_palette(), handle_mouse=False, event_loop=urwid.TwistedEventLoop(), unhandled_input=self.unhandeled_input) self.mainloop.screen.set_terminal_properties(colors=colourmode) - self.show_statusbar = config.getboolean('general', 'show_statusbar') + self.show_statusbar = settings.get('show_statusbar') self.notificationbar = None self.mode = 'global' self.commandprompthistory = [] @@ -169,7 +162,8 @@ class UI(object): ('fixed', len(prefix), leftpart), ('weight', 1, editpart), ]) - both = urwid.AttrMap(both, 'global_prompt') + att = settings.get_theming_attribute('global', 'prompt') + both = urwid.AttrMap(both, att) # put promptwidget as overlay on main widget overlay = urwid.Overlay(both, oldroot, @@ -208,7 +202,7 @@ class UI(object): logging.debug('UI: closing current buffer %s' % buf) index = buffers.index(buf) buffers.remove(buf) - offset = config.getint('general', 'bufferclose_focus_offset') + offset = settings.get('bufferclose_focus_offset') nextbuffer = buffers[(index + offset) % len(buffers)] self.buffer_focus(nextbuffer) buf.cleanup() @@ -313,7 +307,8 @@ class UI(object): ], dividechars=1) else: # above both = urwid.Pile([msgpart, choicespart]) - both = urwid.AttrMap(both, 'prompt', 'prompt') + att = settings.get_theming_attribute('global', 'prompt') + both = urwid.AttrMap(both, att, att) # put promptwidget as overlay on main widget overlay = urwid.Overlay(both, oldroot, @@ -347,7 +342,8 @@ class UI(object): """ def build_line(msg, prio): cols = urwid.Columns([urwid.Text(msg)]) - return urwid.AttrMap(cols, 'global_notify_' + prio) + att = settings.get_theming_attribute('global', 'notify_' + prio) + return urwid.AttrMap(cols, att) msgs = [build_line(message, priority)] if not self.notificationbar: @@ -374,7 +370,7 @@ class UI(object): else: if timeout >= 0: if timeout == 0: - timeout = config.getint('general', 'notify_timeout') + timeout = settings.get('notify_timeout') self.mainloop.set_alarm_in(timeout, clear) return msgs[0] @@ -420,7 +416,8 @@ class UI(object): columns = urwid.Columns([ footerleft, ('fixed', len(righttxt), footerright)]) - return urwid.AttrMap(columns, 'global_footer') + footer_att = settings.get_theming_attribute('global', 'footer') + return urwid.AttrMap(columns, footer_att) def apply_command(self, cmd): """ @@ -437,9 +434,7 @@ class UI(object): if cmd.prehook: logging.debug('calling pre-hook') try: - cmd.prehook(ui=self, dbm=self.dbman, aman=self.accountman, - config=config) - + cmd.prehook(ui=self, dbm=self.dbman) except: logging.exception('prehook failed') @@ -448,8 +443,7 @@ class UI(object): if cmd.posthook: logging.debug('calling post-hook') try: - cmd.posthook(ui=self, dbm=self.dbman, - aman=self.accountman, config=config) + cmd.posthook(ui=self, dbm=self.dbman) except: logging.exception('posthook failed') diff --git a/alot/widgets.py b/alot/widgets.py index a85e31e8..01e7363e 100644 --- a/alot/widgets.py +++ b/alot/widgets.py @@ -1,7 +1,7 @@ import urwid import logging -from settings import config +from settings import settings from helper import shorten_author_string from helper import pretty_datetime from helper import tag_cmp @@ -75,14 +75,11 @@ class ThreadlineWidget(urwid.AttrMap): self.thread = dbman.get_thread(tid) #logging.debug('tid: %s' % self.thread) self.tag_widgets = [] - self.display_content = config.getboolean('general', - 'display_content_in_threadline') - self.highlight_components = config.getstringlist('highlighting', - 'components') - self.highlight_rules = config.get_highlight_rules() + self.display_content = settings.get('display_content_in_threadline') self.rebuild() - urwid.AttrMap.__init__(self, self.columns, - 'search_thread', 'search_thread_focus') + normal = settings.get_theming_attribute('search', 'thread') + focussed = settings.get_theming_attribute('search', 'thread_focus') + urwid.AttrMap.__init__(self, self.columns, normal, focussed) def rebuild(self): cols = [] @@ -93,12 +90,11 @@ class ThreadlineWidget(urwid.AttrMap): if newest == None: datestring = u' ' * 10 else: - if config.has_option('general', 'timestamp_format'): - formatstring = config.get('general', 'timestamp_format') - datestring = newest.strftime(formatstring) - else: + formatstring = settings.get('timestamp_format') + if formatstring is None: datestring = pretty_datetime(newest).rjust(10) - self.highlight_theme_suffix = self._get_highlight_theme_suffix() + else: + datestring = newest.strftime(formatstring) self.date_w = urwid.AttrMap(urwid.Text(datestring), self._get_theme('date')) cols.append(('fixed', len(datestring), self.date_w)) @@ -112,7 +108,7 @@ class ThreadlineWidget(urwid.AttrMap): cols.append(('fixed', len(mailcountstring), self.mailcount_w)) if self.thread: - self.tag_widgets = [TagWidget(t, self.highlight_theme_suffix) + self.tag_widgets = [TagWidget(t) for t in self.thread.get_tags()] else: self.tag_widgets = [] @@ -125,7 +121,7 @@ class ThreadlineWidget(urwid.AttrMap): authors = self.thread.get_authors() or '(None)' else: authors = '(None)' - maxlength = config.getint('general', 'authors_maxlength') + maxlength = settings.get('authors_maxlength') authorsstring = shorten_author_string(authors, maxlength) self.authors_w = urwid.AttrMap(urwid.Text(authorsstring), self._get_theme('authors')) @@ -158,7 +154,6 @@ class ThreadlineWidget(urwid.AttrMap): self.original_widget = self.columns def render(self, size, focus=False): - self.highlight_theme_suffix = self._get_highlight_theme_suffix() if focus: self.date_w.set_attr_map({None: self._get_theme('date', focus)}) self.mailcount_w.set_attr_map({None: @@ -192,28 +187,11 @@ class ThreadlineWidget(urwid.AttrMap): def get_thread(self): return self.thread - def _get_highlight_theme_suffix(self): - suffix = None - for query in self.highlight_rules.keys(): - if self.thread.matches(query): - suffix = self.highlight_rules[query] - break - return suffix - def _get_theme(self, component, focus=False): - theme = 'search_thread_{0}'.format(component) - if (self.highlight_theme_suffix and - component in self.highlight_components): - highlight_theme = (theme + - '_{id}'.format(id=self.highlight_theme_suffix)) - if focus: - theme += '_focus' - highlight_theme += '_focus' - if config.has_theming(highlight_theme): - theme = highlight_theme - elif focus: - theme = theme + '_focus' - return theme + attr_key = 'thread_{0}'.format(component) + if focus: + attr_key += '_focus' + return settings.get_theming_attribute('search', attr_key) class BufferlineWidget(urwid.Text): @@ -244,14 +222,14 @@ class TagWidget(urwid.AttrMap): It looks up the string it displays in the `tag-translate` section of the config as well as custom theme settings for its tag. """ - def __init__(self, tag, theme=''): + def __init__(self, tag): self.tag = tag - self.highlight = theme - self.translated = config.get('tag-translate', tag, fallback=tag) - self.txt = urwid.Text(self.translated.encode('utf-8'), wrap='clip') - normal = config.get_tag_theme(tag, highlight=theme) - focus = config.get_tag_theme(tag, focus=True, highlight=theme) - urwid.AttrMap.__init__(self, self.txt, normal, focus) + representation = settings.get_tagstring_representation(tag) + self.translated = representation['translated'] + self.txt = urwid.Text(self.translated, wrap='clip') + self.normal_att = representation['normal'] + self.focus_att = representation['focussed'] + urwid.AttrMap.__init__(self, self.txt, self.normal_att, self.focus_att) def width(self): # evil voodoo hotfix for double width chars that may @@ -268,14 +246,10 @@ class TagWidget(urwid.AttrMap): return self.tag def set_focussed(self): - self.set_attr_map({None: config.get_tag_theme( - self.tag, focus=True, - highlight=self.highlight)}) + self.set_attr_map({None: self.focus_att}) def set_unfocussed(self): - self.set_attr_map({None: config.get_tag_theme( - self.tag, - highlight=self.highlight)}) + self.set_attr_map({None: self.normal_att}) class ChoiceWidget(urwid.Text): @@ -412,7 +386,7 @@ class MessageWidget(urwid.WidgetWrap): # set available and to be displayed headers self._all_headers = self.mail.keys() - displayed = config.getstringlist('general', 'displayed_headers') + displayed = settings.get('displayed_headers') self._filtered_headers = [k for k in displayed if k in self.mail] self._displayed_headers = None @@ -590,9 +564,9 @@ class MessageSummaryWidget(urwid.WidgetWrap): self.message = message self.even = even if even: - attr = 'thread_summary_even' + attr = settings.get_theming_attribute('thread', 'summary_even') else: - attr = 'thread_summary_odd' + attr = settings.get_theming_attribute('thread', 'summary_odd') cols = [] sumstr = self.__str__() @@ -605,8 +579,9 @@ class MessageSummaryWidget(urwid.WidgetWrap): tag_widgets.sort(tag_cmp, lambda tag_widget: tag_widget.translated) for tag_widget in tag_widgets: cols.append(('fixed', tag_widget.width(), tag_widget)) + focus_att = settings.get_theming_attribute('thread', 'summary_focus') line = urwid.AttrMap(urwid.Columns(cols, dividechars=1), attr, - 'thread_summary_focus') + focus_att) urwid.WidgetWrap.__init__(self, line) def __str__(self): @@ -636,7 +611,8 @@ class HeadersList(urwid.WidgetWrap): def __init__(self, headerslist): self.headers = headerslist pile = urwid.Pile(self._build_lines(headerslist)) - pile = urwid.AttrMap(pile, 'thread_header') + att = settings.get_theming_attribute('thread', 'header') + pile = urwid.AttrMap(pile, att) urwid.WidgetWrap.__init__(self, pile) def __str__(self): @@ -645,6 +621,8 @@ class HeadersList(urwid.WidgetWrap): def _build_lines(self, lines): max_key_len = 1 headerlines = [] + key_att = settings.get_theming_attribute('thread', 'header_key') + value_att = settings.get_theming_attribute('thread', 'header_value') #calc max length of key-string for key, value in lines: if len(key) > max_key_len: @@ -652,8 +630,8 @@ class HeadersList(urwid.WidgetWrap): for key, value in lines: ##todo : even/odd keyw = ('fixed', max_key_len + 1, - urwid.Text(('thread_header_key', key))) - valuew = urwid.Text(('thread_header_value', value)) + urwid.Text((key_att, key))) + valuew = urwid.Text((value_att, value)) line = urwid.Columns([keyw, valuew]) headerlines.append(line) return headerlines @@ -669,7 +647,8 @@ class MessageBodyWidget(urwid.AttrMap): def __init__(self, msg): bodytxt = message.extract_body(msg) - urwid.AttrMap.__init__(self, urwid.Text(bodytxt), 'thread_body') + att = settings.get_theming_attribute('thread', 'body') + urwid.AttrMap.__init__(self, urwid.Text(bodytxt), att) class AttachmentWidget(urwid.WidgetWrap): @@ -685,9 +664,10 @@ class AttachmentWidget(urwid.WidgetWrap): self.attachment = attachment if not isinstance(attachment, message.Attachment): self.attachment = message.Attachment(self.attachment) + att = settings.get_theming_attribute('thread', 'attachment') + focus_att = settings.get_theming_attribute('thread', 'attachment_focus') widget = urwid.AttrMap(urwid.Text(self.attachment.__str__()), - 'thread_attachment', - 'thread_attachment_focus') + att, focus_att) urwid.WidgetWrap.__init__(self, widget) def get_attachment(self): diff --git a/docs/Makefile b/docs/Makefile index 29be0741..7668b4da 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -33,8 +33,9 @@ help: @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" -generate_commands: - python source/generate_commands.py > source/usage/commands.rst +generate_meta: + python source/generate_commands.py + python source/generate_configs.py clean: -rm -rf $(BUILDDIR)/* diff --git a/docs/source/api/accounts.rst b/docs/source/api/accounts.rst deleted file mode 100644 index c09b43fa..00000000 --- a/docs/source/api/accounts.rst +++ /dev/null @@ -1,5 +0,0 @@ -Accounts -======== - -.. automodule:: alot.account - :members: diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index 35b4fd7c..69be4f8a 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -10,6 +10,5 @@ API and Development database interface settings - accounts utils commands diff --git a/docs/source/api/interface.rst b/docs/source/api/interface.rst index 0df9d7ca..4c578e10 100644 --- a/docs/source/api/interface.rst +++ b/docs/source/api/interface.rst @@ -42,7 +42,6 @@ input and acts on it: .. autoattribute:: buffers .. autoattribute:: current_buffer .. autoattribute:: dbman - .. autoattribute:: accountman .. automethod:: apply_command .. automethod:: prompt diff --git a/docs/source/api/settings.rst b/docs/source/api/settings.rst index f7272756..69f91097 100644 --- a/docs/source/api/settings.rst +++ b/docs/source/api/settings.rst @@ -1,55 +1,43 @@ -Accessing User Settings -======================= +User Settings +============= .. module:: alot.settings There are four types of user settings: notmuchs and alot's config files, the hooks-file for user provided python code and the mailcap, defining shellcomands as handlers for files of certain mime types. +Alot sets up :class:`SettingsManager` objects to access these user settings uniformly. -Alot sets up :class:`FallbackConfigParser` objects to access the configs -of alot and notmuch`. -Hooks can be accessed via :meth:`AlotConfigParser.get_hook` -and MIME handlers can be looked up using :func:`alot.settings.get_mime_handler`. - -+----------------+-----------------------------------+------------------------------+ -| What | accessible via | Type | -+================+===================================+==============================+ -| alot config | :obj:`alot.settings.config` | :class:`AlotConfigParser` | -+----------------+-----------------------------------+------------------------------+ -| notmuch config | :obj:`alot.settings.notmuchconfig`| :class:`FallbackConfigParser`| -+----------------+-----------------------------------+------------------------------+ - -Through these objects you can access user settings (or their default values -if unset) in the following manner:: - - from alot.settings import config, notmuchconfig - - # alot config - >>> config.getint('general', 'notify_timeout') - 5 - >>> config.getboolean('general', 'show_statusbar') - True - >>> config.getstringlist('general', 'displayed_headers') - [u'From', u'To', u'Cc', u'Bcc', u'Subject'] - - # notmuch config - >>> notmuchconfig.get('user', 'primary_email') - 'patricktotzke@gmail.com' - >>> notmuchconfig.getboolean('maildir', 'synchronize_flags') - True - -Hooks can be looked up using :meth:`AlotConfigParser.get_hook`. +MIME handlers can be looked up via :meth:`SettingsManager.settings.get_mime_handler`, +config values of alot and notmuch's config are accessible using +:meth:`SettingsManager.get` and :meth:`SettingsManager.get_notmuch_setting`. +These methods return either None or the requested value typed as indicated in +the spec files :file:`alot/defaults/*spec`. + +Hooks can be looked up via :meth:`SettingsManager.get_hook`. They are user defined callables that expect to be called with the following parameters: :ui: :class:`~alot.ui.UI` -- the initialized main component :dbm: :class:`~alot.db.DBManager` -- :obj:`ui.dbman` - :aman: :class:`~alot.account.AccountManager` -- :obj:`ui.accountman` - :log: :class:`~logging.Logger` -- :obj:`ui.logger` - :config: :class:`AlotConfigParser` :obj:`alot.settings.config` -.. autoclass:: FallbackConfigParser +.. autoclass:: SettingsManager + :members: + +Accounts +-------- + +.. module:: alot.account +.. autoclass:: Account + :members: +.. autoclass:: SendmailAccount + :members: + +Addressbooks +------------ + +.. autoclass:: AddressBook + :members: +.. autoclass:: MatchSdtoutAddressbook :members: -.. autoclass:: AlotConfigParser +.. autoclass:: AbookAddressBook :members: -.. autofunction:: get_mime_handler diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst deleted file mode 100644 index 1c1d3eec..00000000 --- a/docs/source/configuration.rst +++ /dev/null @@ -1,428 +0,0 @@ -************* -Configuration -************* - - -All configs are optional, but if you want to send mails you need to -specify at least one account section. - -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 `~/.config/alot/config`. -You can find a complete example config with the default values and their decriptions in -`alot/defaults/alot.rc`. - -Note that since ":" is a separator for key-value pairs you need to use "colon" to bind -commands to ":". - -Here is a key for the interpreted sections: - -[general] - global settings: set your editor etc -[account X] - defines properties of account X: (see below) -[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}. - - -Accounts -======== -A sample gmail section looks like this (provided you have configured msmtp accordingly):: - - [account gmail] - realname = Patrick Totzke - address = patricktotzke@gmail.com - aliases = patricktotzke@googlemail.com - sendmail_command = msmtp --account=gmail -t - -Here's a full list of the interpreted keywords in account sections:: - - # used to format the (proposed) From-header in outgoing mails - realname = your name - address = this accounts email address - - # used to clear your addresses/ match account when formating replies - aliases = foobar@myuni.uk;f.bar@myuni.uk;f.b100@students.myuni.uk - - # how to send mails - sendmail_command = command, defaults to 'sendmail' - - # where to store outgoing mail - sent_box = maildir:///home/you/mail//Sent - - # how to tag sent mails [default: sent]. seperate multiple tags with ','. - sent_tags = sent - - # path to signature file - signature = ~/your_vcard_for_this_account.vcs - - # attach signature file if set to True, append its content (mimetype text) - # to the body text if set to False. Defaults to False. - signature_as_attachment = False - - # signature file's name as it appears in outgoing mails if - # signature_as_attachment is set to True - signature_filename = you.vcs - - # command to lookup contacts - abook_command = abook --mutt-query - abook_regexp = regexp to match name & address in abook_commands output. - -.. warning:: - - 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_box` 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<email>.+?@.+?)\s+(?P<name>.+)`, which makes it work nicely with `abook --mutt-query`. -You can tune this using the `abook_regexp` option (beware Commandparsers escaping semantic!). -Have a look at the FAQ for other examples. - - -Key Bindings -============ -If you want to bind a commandline to a key you can do so by adding the pair to the -`[MODE-maps]` config section, where MODE is the buffer mode you want the binding to hold. -Consider the following lines, which allow you to send mails in envelope buffers using the -combination `control` + `s`:: - - [envelope-maps] - ctrl s = send - -Possible MODE strings are: - -* envelope -* search -* thread -* taglist -* bufferlist -* global - -Bindings defined in section `[global-maps]` are valid in all modes. - -Have a look at `the urwid User Input documentation <http://excess.org/urwid/wiki/UserInput>`_ on how key strings are formated. - - - -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 `~/.config/alot/hooks.py`. -For every command X in mode M, the callables 'pre_M_X' and 'post_M_X' --- if defined -- will be called before and after the command is applied respectively. - -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 -config - `alot.settings.config`, a configparser to access the users config - -An autogenerated API doc for these can be found at http://alot.rtfd.org , -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:: - - import logging - def pre_global_exit(aman=None, **rest): - accounts = aman.get_accounts() - if accounts: - logging.info('goodbye, %s!' % accounts[0].realname) - else: - logging.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) -`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. - - - -Widget Colours -============== -Alot can be run in 1, 16 or 256 colour mode. -The requested mode is determined by the commandline parameter `-C` or read from -option `colourmode` in section `[globals]` of your config file. -The default is 256, which will be scaled down depending on how many colours -your terminal supports. - -The interface will theme its widgets according to the palette defined in -section `[MODEc-theme]` where `MODE` is the integer indicating the colour mode. -Have a look at the default config (`alot/defaults/alot.rc`) for a complete list -of interpreted widget settings; the keys in this section should be self-explanatory. - -Values can be colour names (`light red`, `dark green`..), RGB colour codes (e.g. `#868`), -font attributes (`bold`, `underline`, `blink`, `standout`) or a comma separated combination of -colour and font attributes. -In sections `[16c-theme]` and `[256c-theme]` you can define Y_fg and -Y_bg for the foreground and background of each widget keyword Y, whereas the monochromatic -(`[1c-theme]`) palette can only interpret font attributes for key Y without the suffix. -As an example, check the setting below that makes the footer line appear as -underlined bold red text on a bright green background:: - - [256c-theme] - global_footer_bg = #8f6 - global_footer_fg = light red, bold, underline - -See `urwids docs on Attributes <http://excess.org/urwid/reference.html#AttrSpec>`_ for more details -on the interpreted values. Urwid provides a `neat colour picker script`_ that makes choosing -colours easy. - -.. _neat colour picker script: http://excess.org/urwid/browser/palette_test.py - - -Custom Tagstring Formatting -=========================== -In theme sections you can use keys with prefix `tag_` to format specific tagstrings. For instance, -the following will make alot 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 for some fancy symbols. I personally display my maildir flags -like this:: - - [tag-translate] - flagged = ⚑ - unread = ✉ - replied = ⇄ - -Highlighting Search Results -=========================== -Thread lines in the ``SearchBuffer`` can be highlighted by applying a theme different -from their regular one if they match a `notmuch` query. - -The default config predefines highlighting for threads that carry the `unread`, -the `flagged` or both of those tags. - -Thread lines consist of up to six components (not all of which are shown by -default) that may be themed individually to provide highlighting. The components -are - - - `date` - - `mailcount` - - `tags` - - `authors` - - `subject` - - `content` - -Have a look at Alot's interface to see what they are. - -Customizing highlighting, you may define which components you want highlighted. -Add a `highlighting` section to your config file and define a comma separated -list of highlightable components: :: - - [highlighting] - components = date, mailcount, tags, authors, subject - -Rules ------ -To specify which threads should be highlighted, you need to define highlighting -rules. Rules map queries onto theme identifiers. Each thread that matches a given rule -will use a theme identified by the ID the rule is mapped to. - -.. admonition:: Example - - To highlight threads that are tagged as 'important', add the `rules` - key to your `highlighting` section and provide a dict in JSON syntax. Use an - appropriate `notmuch` query as a key and select a meaningful theme identifier as - its value: - -:: - - rules = { "tag:important":"isimportant" } - -.. note:: - Please make sure the identifier isn't the name of an actual tag, since this - may introduce ambiguity when highlighting tags. More on that `later`_. - -If you want highlighting for other threads as well, just add more rules to the -dict: :: - - rules = { "tag:important":"isimportant", - "subject:alot":"concernsalot", - "from:mom@example.com":"frommom"} - -.. note:: - The sequence of the list defines the search order. The first query that - matches selects the highlighting. So if you have queries that are harder to - satisfy, you should put them earlier in the dict than ones that match more - easily: - -:: - - rules = { "tag:unread":"isunread", - "tag:unread AND tag:important":"isunreadimportant"} - -This setup will never highlight any threads as `isunreadimportant`, since alle -threads that would match that identifier's query will *also* have matched the -`isunread` query earlier in the rules dict. So, again, make sure that rules that -are hard to satisfy show up early in the dict: :: - - rules = { "tag:unread AND tag:important":"isunreadimportant", - "tag:unread":"isunread"} - -This way only threads that didn't match `isunreadimportant` before end up -highlighted as `isunread` only. - -.. _later: `ambiguous theme identifiers`_ - -Theme Generic Components ------------------------- -.. note:: - The following schema will allow you to define highlighting themes for all - components *except* `tags`, which follow a different system and will be - explained in the `next section`_. - -To define a highlighting theme for a component, you need to add a key of the -following format to your colour theme (please cf. `Widget Colours`_ for more information -on theming): :: - - search_thread_COMPONENT_ID_[focus_][fg|bg] - -where - - - ``COMPONENT`` is the component this theme is meant to highlight, - - ``ID`` is the theme identifier that defines which query this option belongs - to, - - ``focus_`` is optional and if present defines that the theme should only be - used if the current thread is focussed and - - ``fg`` or ``bg`` is a selection that specifies which themable part of the - component this option refers to. - -.. admonition:: Example - - The following option will highlight the `subject` of each thread that - matches the query mapping to `isimportant` if the current thread is - `focus`\sed by theming its `foreground` according to the values stated - below: - -:: - - search_thread_subject_isimportant_focus_fg = dark red, underline - -Following this pattern will allow you to set theming for the `background`, for -the `subject` of threads tagged as `important` that are currently not focussed -(by omitting the `focus_` part of the key string), for `subject`\s of threads -matching a different query, and all other components except `tags`. - -.. _next section: `Theme Tags Component`_ - -Theme `Tags` Component ----------------------- -As described in `Custom Tagstring Formatting`_, tags may be themed individually. -Highlighting expands this concept by allowing default themed tags as well as -individual themed tags to provide highlighting variants. - -To specify highlighting themes for default themed tags, just add a key with the wanted -theme identifier: :: - - tag_ID_[focus_][fg|bg] - -where - - - ``ID`` is the theme identifier that defines which query this option belongs - to, - - ``focus_`` is optional and if present defines that the theme should only be - used if the current thread is focussed and - - ``fg`` or ``bg`` is a selection that specifies which themable part of the - component this option refers to. - -To highlight custom themed tags, proceed accordingly. Specify :: - - tag_TAG_ID_[focus_][fg|bg] - -where - - - ``TAG`` is the name of the custom themed tag that is to be highlighted, - - ``ID`` is the theme identifier that defines which query this option belongs - to, - - ``focus_`` is optional and if present defines that the theme should only be - used if the current thread is focussed and - - ``fg`` or ``bg`` is a selection that specifies which themable part of the - component this option refers to. - -.. _ambiguous theme identifiers: -.. caution:: - As mentioned earlier, using tag names as theme identifiers may introduce - ambiguity and lead to unexpected theming results. - -Assuming one would replace the theme identifier `isimportant` with its intuitive -alternative `important`, the tag theme ``tag_important_fg`` might either be a -custom theme for the tag `important` of the form ``tag_TAG_fg`` or the highlight -theme for default themed tags of threads that match the query that maps to the -`important` identifier: ``tag_ID_fg``. - -Using above proper identifier would distinguish those options as -``tag_important_fg`` for the custom theme and ``tag_isimportant_fg`` for the -highlighting theme. - - -Contacts Completion -=================== -In each `account` section you can specify a `abook_command` that -is considered the address book of that account and will be used -for address completion where appropriate. - -This shell command will be called with the search prefix as only argument. -Its output is searched for email-name pairs using the regular expression given as `abook_regexp`, -which must include named groups "email" and "name" to match the email address and realname parts -respectively. See below for an example that uses `abook <http://abook.sourceforge.net/>`_:: - - [account YOURACCOUNT] - realname = ... - address = ... - abook_command = abook --mutt-query - abook_regexp = '(?P<email>.+?@.+?)\s+(?P<name>.+?)\s*$' - -See `here <http://notmuchmail.org/emacstips/#index11h2>`_ for alternative lookup commands. The few others I have tested so far are: - -`goobook <http://code.google.com/p/goobook/>`_ - for cached google contacts lookups:: - - abook_command = goobook query - abook_regexp = (?P<email>.+?@.+?)\s\s+(?P<name>.+)\s\s+.+ - -`nottoomuch-addresses <http://www.iki.fi/too/nottoomuch/nottoomuch-addresses/>`_ - completes contacts found in the notmuch index:: - - abook_command = nottoomuch-addresses.sh - abook_regexp = \"(?P<name>.+)\"\s*<(?P<email>.*.+?@.+?)> - -Don't hesitate to send me your custom `abook_regexp` values to list them here. diff --git a/docs/source/configuration/accounts_table.rst b/docs/source/configuration/accounts_table.rst new file mode 100644 index 00000000..efa7698b --- /dev/null +++ b/docs/source/configuration/accounts_table.rst @@ -0,0 +1,51 @@ + +.. describe:: address + + your email address + +.. describe:: realname + + used to format the (proposed) From-header in outgoing mails + +.. describe:: aliases + + used to clear your addresses/ match account when formating replies + +.. describe:: sendmail_command + + how to send mails + +.. describe:: sent_box + + specifies the mailbox where you want outgoing mails to be stored after successfully sending them, e.g. + where to store outgoing mail, e.g. `maildir:///home/you/mail//Sent` + You can use mbox, maildir, mh, babyl and mmdf in the protocol part of the url. + +.. describe:: sent_tags + + how to tag sent mails. + +.. describe:: signature + + path to signature file that gets attached to all outgoing mails from this account, optionally + renamed to `signature_filename`. + +.. describe:: signature_as_attachment + + attach signature file if set to True, append its content (mimetype text) + to the body text if set to False. Defaults to False. + +.. describe:: signature_filename + + signature file's name as it appears in outgoing mails if + signature_as_attachment is set to True + +.. describe:: abook_command + + command to lookup contacts. + 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. + +.. describe:: abook_regexp + + The regular expression used to match name/address pairs in the output of `abook_command` diff --git a/docs/source/configuration/alotrc_table.rst b/docs/source/configuration/alotrc_table.rst new file mode 100644 index 00000000..abb18e5e --- /dev/null +++ b/docs/source/configuration/alotrc_table.rst @@ -0,0 +1,125 @@ + +.. describe:: ask_subject + + ask for subject when compose + +.. describe:: bug_on_exit + + confirm exit + +.. describe:: bufferclose_focus_offset + + offset of next focussed buffer if the current one gets closed + +.. describe:: colourmode + + number of colours your terminal supports + +.. describe:: tabwidth + + number of spaces used to replace tab characters + +.. describe:: template_dir + + templates directory that contains your message templates. + It will be used if you give `compose --template` a filename without a path prefix. + +.. describe:: display_content_in_threadline + + fill threadline with message content + +.. describe:: displayed_headers + + headers that get displayed by default + +.. describe:: envelope_headers_blacklist + + headers that are hidden in envelope buffers by default + +.. describe:: terminal_cmd + + set terminal command used for spawning shell commands + +.. describe:: editor_cmd + + editor command + if unset, alot will first try the EDITOR env variable, then /usr/bin/editor + +.. describe:: editor_writes_encoding + + file encoding used by your editor + +.. describe:: editor_spawn + + use terminal_command to spawn a new terminal for the editor? + +.. describe:: editor_in_thread + + call editor in separate thread. + In case your editor doesn't run in the same window as alot, setting true here + will make alot non-blocking during edits + +.. describe:: edit_headers_whitelist + + Which header fields should be editable in your editor + used are those that match the whitelist and don't macht the blacklist. + in both cases '*' may be used to indicate all fields. + +.. describe:: edit_headers_blacklist + + +.. describe:: flush_retry_timeout + + timeout in secs after a failed attempt to flush is repeated + +.. describe:: hooksfile + + where to look up hooks + +.. describe:: notify_timeout + + time in secs to display status messages + +.. describe:: show_statusbar + + display statusline? + +.. describe:: timestamp_format + + timestamp format in strftime format syntax: + http://docs.python.org/library/datetime.html#strftime-strptime-behavior + +.. describe:: authors_maxlength + + max length of authors line in thread widgets + +.. describe:: print_cmd + + how to print messages: + this specifies a shellcommand used pro printing. + threads/messages are piped to this as plaintext. + muttprint/a2ps works nicely + +.. describe:: initial_command + + initial command when none is given as argument: + +.. describe:: search_threads_sort_order + + default sort order of results in a search + +.. describe:: complete_matching_abook_only + + in case more than one account has an address book: + Set this to True to make tabcompletion for recipients during compose only + look in the abook of the account matching the sender address + +.. describe:: quit_on_last_bclose + + shut down when the last buffer gets closed + +.. describe:: user_agent + + value of the User-Agent header used for outgoing mails. + setting this to the empty string will cause alot to omit the header all together. + The string '$VERSION' will be replaced by the version string of the running instance. diff --git a/docs/source/configuration/index.rst b/docs/source/configuration/index.rst new file mode 100644 index 00000000..285725f5 --- /dev/null +++ b/docs/source/configuration/index.rst @@ -0,0 +1,266 @@ +************* +Configuration +************* + +Alot reads a config file in "INI" syntax: +It consists of key-value pairs that use "=" as separator and '#' is comment-prefixes. +Sections and subsections are defined using square brackets. +The default location for the config file is :file:`~/.config/alot/config`. + +Config options +============== + +.. include:: alotrc_table.rst + +.. _account: + +Accounts +======== +In order to be able to send mails, you have to define at least one account subsection in your config: +There needs to be a section "accounts", and each subsection, indicated by double square brackets defines an account. + +Here is an example configuration:: + + [accounts] + [[work]] + realname = Bruce Wayne + address = b.wayne@wayneenterprises.com + gpg_key = D7D6C5AA + sendmail_command = msmtp --account=wayne -t + sent_box = maildir:///home/bruce/mail/work/Sent + draft_box = maildir:///home/bruce/mail/work/Drafts + + [[secret]] + realname = Batman + address = batman@batcave.org + aliases = batman@batmobile.org + sendmail_command = msmtp --account=batman -t + signature = ~/.batman.vcf + signature_as_attachment = True + +.. warning:: + + 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`. + +The following entries are interpreted at the moment: + +.. include:: accounts_table.rst + + +Contacts Completion +=================== +In each :ref:`account` section you can specify a `abook_command` that +is considered the address book of that account and will be used +for address completion where appropriate. + +This shell command will be called with the search prefix as only argument. +Its output is searched for email-name pairs using the regular expression given as `abook_regexp`, +which must include named groups "email" and "name" to match the email address and realname parts +respectively. See below for an example that uses `abook <http://abook.sourceforge.net/>`_:: + + [accounts] + [[youraccount]] + ... + abook_command = abook --mutt-query + abook_regexp = '(?P<email>.+?@.+?)\s+(?P<name>.+?)\s*$' + +See `here <http://notmuchmail.org/emacstips/#index11h2>`_ for alternative lookup commands. The few others I have tested so far are: + +`goobook <http://code.google.com/p/goobook/>`_ + for cached google contacts lookups:: + + abook_command = goobook query + abook_regexp = (?P<email>.+?@.+?)\s\s+(?P<name>.+)\s\s+.+ + +`nottoomuch-addresses <http://www.iki.fi/too/nottoomuch/nottoomuch-addresses/>`_ + completes contacts found in the notmuch index:: + + abook_command = nottoomuch-addresses.sh + abook_regexp = \"(?P<name>.+)\"\s*<(?P<email>.*.+?@.+?)> + +Don't hesitate to send me your custom `abook_regexp` values to list them here. + + + +Key Bindings +============ +If you want to bind a commandline to a key you can do so by adding the pair to the +`[bindings]` section. This will introduce a *global* binding, that works in +all modes. To make a binding specific to a mode you have to add the pair +under the subsection named like the mode. For instance, +if you want to bind `T` to open a new search for threads tagged with 'todo', +and be able to toggle this tag in search mode, you'd add this to your config:: + + [bindings] + T = search tag:todo + + [[search]] + t = toggletags todo + +.. _modes: + +Known modes are: + +* envelope +* search +* thread +* taglist +* bufferlist +* global + +Have a look at `the urwid User Input documentation <http://excess.org/urwid/wiki/UserInput>`_ on how key strings are formated. + + +Hooks +===== +Hooks are python callables that live in a module specified by `hooksfile` in the +config. Per default this points to :file:`~/.config/alot/hooks.py`. +When a hook gets called it receives a reference to the :class:`main user interface <alot.ui.UI>` and the +:class:`database manager <alot.db.DBManager>`. +For every :doc:`COMMAND <../usage/commands>` in mode :ref:`MODE <modes>`, the callables :func:`pre_MODE_COMMAND` and :func:`post_MODE_COMMAND` +-- if defined -- will be called before and after the command is applied respectively. The signature for the +pre-`send` hook in envelope mode for example looks like this: + +.. py:function:: pre_envelope_send(ui=None, dbm=None) + + :param ui: the main user interface + :type ui: :class:`alot.ui.UI` + :param dbm: a database manager + :type dbm: :class:`alot.db.DBManager` + +Consider this pre-hook for the exit command, that logs a personalized goodby message:: + + import logging + from alot.settings import settings + def pre_global_exit(ui, dbm): + accounts = settings.get_accounts() + if accounts: + logging.info('goodbye, %s!' % accounts[0].realname) + else: + logging.info('goodbye!') + +Apart from command pre and posthooks, the following hooks will be interpreted: + +.. py:function:: reply_prefix(realname, address, timestamp[, ui= None, dbm=None]) + + Is used to reformat the first indented line in a reply message. + This defaults to 'Quoting %s (%s)\n' % (realname, timestamp)' unless this hook is defined + + :param realname: name or the original sender + :type realname: str + :param address: address of the sender + :type address: str + :param timestamp: value of the Date header of the replied message + :type timestamp: :obj:`datetime.datetime` + :rtype: string + +.. py:function:: forward_prefix(realname, address, timestamp[, ui= None, dbm=None]) + + Is used to reformat the first indented line in a inline forwarded message. + This defaults to 'Forwarded message from %s (%s)\n' % (realname, timestamp)' if this hook is undefined + + :param realname: name or the original sender + :type realname: str + :param address: address of the sender + :type address: str + :param timestamp: value of the Date header of the replied message + :type timestamp: :obj:`datetime.datetime` + :rtype: string + +.. py:function:: pre_edit_translate(bodytext[, ui= None, dbm=None]) + + used to manipulate a messages bodytext *before* the editor is called. + + :param bodytext: text representation of the mail body as displayed in the ui and as send to the editor + :type bodytext: str + :rtype: str + +.. py:function:: post_edit_translate(bodytext[, ui= None, dbm=None]) + + used to manipulate a messages bodytext *after* the editor is called + + :param bodytext: text representation of the mail body as displayed in the ui and as send to the editor + :type bodytext: str + :rtype: str + + +Themes +====== +Alot can be run in 1, 16 or 256 colour mode. The requested mode is determined by the commandline parameter `-C` or read +from option `colourmode` config value. The default is 256, which scales down depending on how many colours your +terminal supports. + +To specify the theme to use, set the `theme` config option to the name of a theme-file. +A file by that name will be looked up in the path given by the `themes_dir` config setting +which defaults to :file:`~/.config`. + +Theme-files can contain sections `[1], [16]` and `[256]` for different colour modes, +each of which has subsections named after the :ref:`MODE <modes>` they are used in +plus "help" for the bindings-help overlay and "global" for globally used themables +like footer, prompt etc. +The themables live in sub-sub-sections and define the attributes `fg` and `bg` for foreground +and backround colours and attributes, the names of the themables should be self-explanatory. +Have a look at the default theme file at :file:`alot/defaults/default.theme` +and the config spec :file:`alot/defaults/default.theme` for the format. + +As an example, check the setting below that makes the footer line appear as +underlined bold red text on a bright green background:: + + [256] + [[global]] + [[[footer]]] + fg = light red, bold, underline + bg = #8f6 + +Values can be colour names (`light red`, `dark green`..), RGB colour codes (e.g. `#868`), +font attributes (`bold`, `underline`, `blink`, `standout`) or a comma separated combination of +colour and font attributes. + +.. note:: In monochromatic mode only the entry `fg` is interpreted. It may only contain + (a comma-separated list of) font attributes: 'bold', 'underline', 'blink', 'standout'. + +See `urwids docs on Attributes <http://excess.org/urwid/reference.html#AttrSpec>`_ for more details +on the interpreted values. Urwid provides a `neat colour picker script`_ that makes choosing +colours easy. + +.. _neat colour picker script: http://excess.org/urwid/browser/palette_test.py + + +Custom Tagstring Formatting +=========================== + +To specify how a particular tgstring is displayes throughout the interface you can +add a subsection named after the tag to the `[tags]` config section. +The following attribute keys will interpreted and may contain urwid attribute strings +as described in the :ref:`Themes` section above: + +`fg` (foreground), `bg` (background), `focus_fg` (foreground if focussed) and `focus_bg` (background if focussed). +An alternative string representation is read from the option `translated`. + +The following will make alot display the "todo" tag as "TODO" in white on red. :: + + [tags] + [[todo]] + bg = #d66 + fg = white + translated TODO + +Utf-8 symbols are welcome here, see e.g. +http://panmental.de/symbols/info.htm for some fancy symbols. I personally display my maildir flags +like this:: + + [tags] + [[flagged]] + translated = ⚑ + fg = light red + + [[unread]] + translated= ✉ + fg = white + + [[replied]] + translated= ⏎ + + [[encrypted]] + translated= ⚷ diff --git a/docs/source/generate_configs.py b/docs/source/generate_configs.py new file mode 100755 index 00000000..90cf0cf9 --- /dev/null +++ b/docs/source/generate_configs.py @@ -0,0 +1,29 @@ +import sys +import os +HERE = os.path.dirname(__file__) +sys.path.append(os.path.join(HERE, '..', '..', '..')) +from alot.commands import COMMANDS +from configobj import ConfigObj +import re + +def rewrite_scalarcomments(config, path): + file = open(path, 'w') + for entry in config.scalars: + description = '\n.. describe:: %s\n\n' % entry + comments = [config.inline_comments[entry]] + config.comments[entry] + for c in comments: + if c: + description += ' '*4 + re.sub('^\s*#\s*', '', c) + '\n' + file.write(description) + file.close() + +if __name__ == "__main__": + specpath = os.path.join(HERE, '..','..', 'alot', 'defaults', 'alot.rc.spec') + config = ConfigObj(specpath) + + alotrc_table_file = os.path.join(HERE, 'configuration', 'alotrc_table.rst') + rewrite_scalarcomments(config, alotrc_table_file) + + rewrite_scalarcomments(config['accounts']['__many__'], + os.path.join(HERE, 'configuration', + 'accounts_table.rst')) diff --git a/docs/source/index.rst b/docs/source/index.rst index 03dacb4b..adcb8425 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,5 +12,5 @@ User Manual :numbered: usage/index - configuration + configuration/index api/index @@ -12,7 +12,13 @@ setup(name='alot', url=alot.__url__, license=alot.__copyright__, packages=['alot', 'alot.commands'], - package_data={'alot': ['defaults/alot.rc', 'defaults/notmuch.rc']}, + package_data={'alot': [ + 'defaults/alot.rc.spec', + 'defaults/notmuch.rc.spec', + 'defaults/default.theme', + 'defaults/bindings', + 'defaults/theme.spec', + ]}, scripts=['bin/alot'], requires=[ 'notmuch (>=0.9)', @@ -20,6 +26,7 @@ setup(name='alot', 'urwid (>=1.0)', 'twisted (>=10.2.0)', 'magic', + 'configobj', 'subprocess (>=2.7)'], provides='alot', ) |