summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--alot/account.py123
-rw-r--r--alot/buffers.py17
-rw-r--r--alot/commands/__init__.py7
-rw-r--r--alot/commands/envelope.py36
-rw-r--r--alot/commands/globals.py50
-rw-r--r--alot/commands/thread.py23
-rw-r--r--alot/completion.py32
-rw-r--r--alot/db.py4
-rw-r--r--alot/defaults/alot.rc437
-rw-r--r--alot/defaults/alot.rc.new10
-rw-r--r--alot/defaults/alot.rc.spec162
-rw-r--r--alot/defaults/bindings65
-rw-r--r--alot/defaults/default.theme317
-rw-r--r--alot/defaults/notmuch.rc.spec3
-rw-r--r--alot/defaults/theme.spec317
-rw-r--r--alot/helper.py7
-rwxr-xr-xalot/init.py66
-rw-r--r--alot/message.py17
-rw-r--r--alot/settings.py588
-rw-r--r--alot/ui.py40
-rw-r--r--alot/widgets.py100
-rw-r--r--docs/Makefile5
-rw-r--r--docs/source/api/accounts.rst5
-rw-r--r--docs/source/api/index.rst1
-rw-r--r--docs/source/api/interface.rst1
-rw-r--r--docs/source/api/settings.rst70
-rw-r--r--docs/source/configuration.rst428
-rw-r--r--docs/source/configuration/accounts_table.rst51
-rw-r--r--docs/source/configuration/alotrc_table.rst125
-rw-r--r--docs/source/configuration/index.rst266
-rwxr-xr-xdocs/source/generate_configs.py29
-rw-r--r--docs/source/index.rst2
-rwxr-xr-xsetup.py9
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()
diff --git a/alot/db.py b/alot/db.py
index a4dfe998..44ca608d 100644
--- a/alot/db.py
+++ b/alot/db.py
@@ -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()
diff --git a/alot/ui.py b/alot/ui.py
index 4a6b37fe..e218ace4 100644
--- a/alot/ui.py
+++ b/alot/ui.py
@@ -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
diff --git a/setup.py b/setup.py
index 62110fdd..d7225bac 100755
--- a/setup.py
+++ b/setup.py
@@ -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',
)