summaryrefslogtreecommitdiff
path: root/alot/settings/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'alot/settings/__init__.py')
-rw-r--r--alot/settings/__init__.py398
1 files changed, 398 insertions, 0 deletions
diff --git a/alot/settings/__init__.py b/alot/settings/__init__.py
new file mode 100644
index 00000000..593fbae2
--- /dev/null
+++ b/alot/settings/__init__.py
@@ -0,0 +1,398 @@
+import imp
+import os
+import re
+import mailcap
+import logging
+import urwid
+from urwid import AttrSpec, AttrSpecError
+from configobj import ConfigObj, Section
+
+from alot.account import SendmailAccount, MatchSdtoutAddressbook, AbookAddressBook
+
+from errors import ConfigError
+from utils import read_config
+from checks import mail_container
+
+DEFAULTSPATH = os.path.join(os.path.dirname(__file__), '..', 'defaults')
+
+
+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 _parse_attributes(self, c):
+ """
+ parse a (previously validated) valid theme file
+ into urwid AttrSpec attributes for internal use.
+
+ :param c: config object for theme file
+ :type c: `configobj.ConfigObj`
+ :raises: `ConfigError`
+ """
+
+ 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
+
+ :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
+ """
+ return self.attributes[colourmode][mode][name]
+
+
+class SettingsManager(object):
+ """Organizes user settings"""
+ def __init__(self, alot_rc=None, notmuch_rc=None, theme=None):
+ """
+ :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
+ """
+ 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, checks={'mail_container': mail_container})
+ 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:
+ 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:
+ 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_accounts(self, config):
+ """
+ read accounts information from config
+
+ :param config: valit alot config
+ :type config: `configobj.ConfigObj`
+ :returns: list of accounts
+ """
+ accounts = []
+ if 'accounts' in config:
+ for acc in config['accounts'].sections:
+ accsec = config['accounts'][acc]
+ args = dict(config['accounts'][acc])
+
+ # create abook for this account
+ abook = accsec['abook']
+ logging.debug('abook defined: %s' % abook)
+ if abook['type'] == 'shellcommand':
+ cmd = abook['command']
+ regexp = abook['regexp']
+ if cmd is not None and regexp is not None:
+ args['abook'] = MatchSdtoutAddressbook(cmd,
+ match=regexp)
+ else:
+ msg = 'underspecified abook of type \'shellcommand\':'
+ msg += '\ncommand: %s\nregexp:%s' % (cmd, regexp)
+ raise ConfigError(msg)
+ elif abook['type'] == 'abook':
+ contacts_path = abook['abook_contacts_file']
+ args['abook'] = AbookAddressBook(contacts_path)
+ else:
+ del(args['abook'])
+
+ 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):
+ """
+ setter for global config values
+
+ :param key: config option identifise
+ :type key: str
+ :param value: option to set
+ :type value: depends on the specfile :file:`alot.rc.spec`
+ """
+ self._config[key] = value
+
+ def get_notmuch_setting(self, section, key):
+ """
+ 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
+ """
+ value = None
+ if section in self._notmuchconfig:
+ if key in self._notmuchconfig[section]:
+ value = self._notmuchconfig[section][key]
+ return value
+
+ def get_theming_attribute(self, mode, name):
+ """
+ looks up theming attribute
+
+ :param mode: ui-mode (e.g. `search`,`thread`...)
+ :type mode: str
+ :param name: identifier of the atttribute
+ :type name: str
+ """
+ colours = int(self._config.get('colourmode'))
+ return self._theme.get_attribute(mode, name, colours)
+
+ def get_tagstring_representation(self, tag):
+ """
+ looks up user's preferred way to represent a given tagstring
+
+ This returns a dictionary mapping
+ 'normal' and 'focussed' to `urwid.AttrSpec` sttributes,
+ and 'translated' to an alternative string representation
+ """
+ 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)
+ for sec in self._config['tags'].sections:
+ if re.match('^' + sec + '$', tag):
+ fg = self._config['tags'][sec]['fg'] or default.foreground
+ bg = self._config['tags'][sec]['bg'] or default.background
+ try:
+ normal = urwid.AttrSpec(fg, bg, colours)
+ except AttrSpecError:
+ normal = default
+ focus_fg = self._config['tags'][sec]['focus_fg']
+ focus_fg = focus_fg or default_f.foreground
+ focus_bg = self._config['tags'][sec]['focus_bg']
+ focus_bg = focus_bg or default_f.background
+ try:
+ focussed = urwid.AttrSpec(focus_fg, focus_bg, colours)
+ except AttrSpecError:
+ focussed = default_f
+
+ hidden = self._config['tags'][sec]['hidden'] or False
+
+ translated = self._config['tags'][sec]['translated'] or tag
+ translation = self._config['tags'][sec]['translation']
+ if translation:
+ translated = re.sub(translation[0], translation[1], tag)
+ break
+ else:
+ normal = default
+ focussed = default_f
+ hidden = False
+ translated = tag
+
+ return {'normal': normal, 'focussed': focussed,
+ 'hidden': hidden, '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_keybinding(self, mode, key):
+ """look up keybinding from `MODE-maps` sections
+
+ :param mode: mode identifier
+ :type mode: str
+ :param key: urwid-style key identifier
+ :type key: str
+ :returns: a command line to be applied upon keypress
+ :rtype: str
+ """
+ cmdline = None
+ 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
+
+ :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
+
+ 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()