summaryrefslogtreecommitdiff
path: root/alot/settings/theme.py
blob: 0eda72f2e02f99e3da6d6109144a93c5bd964cae (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# Copyright (C) 2011-2012  Patrick Totzke <patricktotzke@gmail.com>
# This file is released under the GNU GPL, version 3 or a later revision.
# For further details see the COPYING file

import os

from .errors import ConfigError
from .utils  import read_config

from ..utils import configobj as checks

DEFAULTSPATH = os.path.join(os.path.dirname(__file__), '..', 'defaults')
DUMMYDEFAULT = ('default',) * 6


class Theme:
    """Colour theme"""
    def __init__(self, path):
        """
        :param path: path to theme file
        :type path: str
        :raises: :class:`~alot.settings.errors.ConfigError`
        """
        self._spec = os.path.join(DEFAULTSPATH, 'theme.spec')
        self._config = read_config(path, self._spec, report_extra=True,
                                   checks={'align': checks.align_mode,
                                           'widthtuple': checks.width_tuple,
                                           'force_list': checks.force_list,
                                           'attrtriple': checks.attr_triple})
        self._colours = [1, 16, 256]
        # make sure every entry in 'order' lists have their own subsections
        threadline = self._config['search']['threadline']
        for sec in self._config['search']:
            if sec.startswith('threadline'):
                tline = self._config['search'][sec]
                if tline['parts'] is not None:
                    listed = set(tline['parts'])
                    here = set(tline.sections)
                    indefault = set(threadline.sections)
                    diff = listed.difference(here.union(indefault))
                    if diff:
                        msg = 'missing threadline parts: %s' % ', '.join(diff)
                        raise ConfigError(msg)

    def get_attribute(self, colourmode, mode, name, part=None):
        """
        returns requested attribute

        :param mode: ui-mode (e.g. `search`,`thread`...)
        :type mode: str
        :param name: of the atttribute
        :type name: str
        :param colourmode: colour mode; in [1, 16, 256]
        :type colourmode: int
        :rtype: urwid.AttrSpec
        """
        thmble = self._config[mode][name]
        if part is not None:
            thmble = thmble[part]
        thmble = thmble or DUMMYDEFAULT
        return thmble[self._colours.index(colourmode)]

    def get_threadline_theming(self, thread, colourmode):
        """
        look up how to display a Threadline wiidget in search mode
        for a given thread.

        :param thread: Thread to theme Threadline for
        :type thread: alot.db.thread.Thread
        :param colourmode: colourmode to use, one of 1,16,256.
        :type colourmode: int

        This will return a dict mapping
            :normal: to `urwid.AttrSpec`,
            :focus: to `urwid.AttrSpec`,
            :parts: to a list of strings indentifying subwidgets
                    to be displayed in this order.

        Moreover, for every part listed this will map 'part' to a dict mapping
            :normal: to `urwid.AttrSpec`,
            :focus: to `urwid.AttrSpec`,
            :width: to a tuple indicating the width of the subpart.
                    This is either `('fit', min, max)` to force the widget
                    to be at least `min` and at most `max` characters wide,
                    or `('weight', n)` which makes it share remaining space
                    with other 'weight' parts.
            :alignment: where to place the content if shorter than the widget.
                        This is either 'right', 'left' or 'center'.
        """
        def pickcolour(triple):
            return triple[self._colours.index(colourmode)]

        def matches(sec, thread):
            if sec.get('tagged_with') is not None:
                if not set(sec['tagged_with']).issubset(thread.get_tags()):
                    return False
            if sec.get('query') is not None:
                if not thread.matches(sec['query']):
                    return False
            return True

        default = self._config['search']['threadline']
        match = default

        candidates = self._config['search'].sections
        for candidatename in candidates:
            candidate = self._config['search'][candidatename]
            if (candidatename.startswith('threadline') and
                    (not candidatename == 'threadline') and
                    matches(candidate, thread)):
                match = candidate
                break

        # fill in values
        res = {}
        res['normal'] = pickcolour(match.get('normal') or default['normal'])
        res['focus'] = pickcolour(match.get('focus') or default['focus'])
        res['parts'] = match.get('parts') or default['parts']
        for part in res['parts']:
            defaultsec = default.get(part)
            partsec = match.get(part) or {}

            def fill(key, fallback=None):
                pvalue = partsec.get(key) or defaultsec.get(key)
                return pvalue or fallback

            res[part] = {}
            res[part]['width'] = fill('width', ('fit', 0, 0))
            res[part]['alignment'] = fill('alignment', 'right')
            res[part]['normal'] = pickcolour(fill('normal'))
            res[part]['focus'] = pickcolour(fill('focus'))
        return res

    def get_quote_theming(self, colourmode):
        """
        Get a list of attributes to use for displaying successive levels of
        nested quotes in a message body.

        :param colourmode: colourmode to use, one of 1,16,256.
        :type colourmode: int
        """
        idx      = self._colours.index(colourmode)
        mainsect = self._config['thread']['quote']

        ret = []
        for sect in sorted(mainsect.sections):
            ret.append(mainsect[sect]['body'][idx])
        return ret