summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Totzke <patricktotzke@gmail.com>2012-07-22 21:55:00 +0100
committerPatrick Totzke <patricktotzke@gmail.com>2012-07-22 21:55:00 +0100
commitdd087f6c0ea083a5b245bf11e2e2b08ee9ede0e0 (patch)
tree0c7728a6dfa348895bcc8660159cef4d333b8702
parent0fc049f04bb9b5a44a379206e37dd32e65feae99 (diff)
parent44a7d6f262bd6eb133202d48e9fd176c81d285be (diff)
Merge branch '0.3.1-theming'
-rw-r--r--alot/addressbooks.py3
-rw-r--r--alot/buffers.py26
-rw-r--r--alot/defaults/alot.rc.spec15
-rw-r--r--alot/defaults/default.theme439
-rw-r--r--alot/defaults/theme.spec412
-rw-r--r--alot/errors.py2
-rw-r--r--alot/helper.py2
-rw-r--r--alot/settings/__init__.py112
-rw-r--r--alot/settings/checks.py75
-rw-r--r--alot/settings/errors.py2
-rw-r--r--alot/settings/theme.py144
-rw-r--r--alot/settings/utils.py44
-rw-r--r--alot/ui.py1
-rw-r--r--alot/widgets.py325
-rw-r--r--docs/source/configuration/index.rst167
-rwxr-xr-xextra/colour_picker.py259
-rwxr-xr-xextra/tagsections_convert.py77
-rwxr-xr-xextra/theme_convert.py124
-rw-r--r--extra/themes/mutt76
-rw-r--r--extra/themes/screenshots/mutt.search.pngbin0 -> 163843 bytes
-rw-r--r--extra/themes/screenshots/mutt.thread.pngbin0 -> 74014 bytes
-rw-r--r--extra/themes/screenshots/solarized.search.pngbin0 -> 221009 bytes
-rw-r--r--extra/themes/screenshots/solarized.thread.pngbin0 -> 93938 bytes
-rw-r--r--extra/themes/screenshots/solarized_dark.search.pngbin0 -> 194417 bytes
-rw-r--r--extra/themes/screenshots/solarized_dark.thread.pngbin0 -> 83319 bytes
-rw-r--r--extra/themes/solarized126
-rw-r--r--extra/themes/solarized_dark127
27 files changed, 1555 insertions, 1003 deletions
diff --git a/alot/addressbooks.py b/alot/addressbooks.py
index 00fe9fe7..a8453b09 100644
--- a/alot/addressbooks.py
+++ b/alot/addressbooks.py
@@ -50,7 +50,8 @@ class AbookAddressBook(AddressBook):
res = []
for id in c.sections:
for email in c[id]['email']:
- if email: res.append((c[id]['name'], email))
+ if email:
+ res.append((c[id]['name'], email))
return res
diff --git a/alot/buffers.py b/alot/buffers.py
index 07a7589d..866e1491 100644
--- a/alot/buffers.py
+++ b/alot/buffers.py
@@ -78,11 +78,11 @@ class BufferlistBuffer(Buffer):
line = widgets.BufferlineWidget(b)
if (num % 2) == 0:
attr = settings.get_theming_attribute('bufferlist',
- 'results_even')
+ 'line_even')
else:
- attr = settings.get_theming_attribute('bufferlist',
- 'results_odd')
- focus_att = settings.get_theming_attribute('bufferlist', 'focus')
+ attr = settings.get_theming_attribute('bufferlist', 'line_odd')
+ focus_att = settings.get_theming_attribute('bufferlist',
+ 'line_focus')
buf = urwid.AttrMap(line, attr, focus_att)
num = urwid.Text('%3d:' % self.index_of(b))
lines.append(urwid.Columns([('fixed', 4, num), buf]))
@@ -385,13 +385,23 @@ class TagListBuffer(Buffer):
displayedtags = sorted(filter(self.filtfun, self.tags),
key=unicode.lower)
for (num, b) in enumerate(displayedtags):
- tw = widgets.TagWidget(b)
+ line = widgets.BufferlineWidget(b)
+ if (num % 2) == 0:
+ attr = settings.get_theming_attribute('taglist', 'line_even')
+ else:
+ attr = settings.get_theming_attribute('taglist', 'line_odd')
+ focus_att = settings.get_theming_attribute('taglist', 'line_focus')
+
+ tw = widgets.TagWidget(b, attr, focus_att)
rows = [('fixed', tw.width(), tw)]
if tw.hidden:
- rows.append(urwid.Text('[hidden]'))
+ rows.append(urwid.Text(b + ' [hidden]'))
elif tw.translated is not b:
rows.append(urwid.Text('(%s)' % b))
- lines.append(urwid.Columns(rows, dividechars=1))
+ line = urwid.Columns(rows, dividechars=1)
+ line = urwid.AttrMap(line, attr, focus_att)
+ lines.append(line)
+
self.taglist = urwid.ListBox(urwid.SimpleListWalker(lines))
self.body = self.taglist
@@ -400,5 +410,5 @@ class TagListBuffer(Buffer):
def get_selected_tag(self):
"""returns selected tagstring"""
(cols, pos) = self.taglist.get_focus()
- tagwidget = cols.get_focus()
+ tagwidget = cols.original_widget.get_focus()
return tagwidget.get_tag()
diff --git a/alot/defaults/alot.rc.spec b/alot/defaults/alot.rc.spec
index cd5dd6c3..174413fd 100644
--- a/alot/defaults/alot.rc.spec
+++ b/alot/defaults/alot.rc.spec
@@ -128,9 +128,6 @@ envelope_statusbar = mixed_list(string, string, default=list('[{buffer_no}: enve
# timestamp format in `strftime format syntax <http://docs.python.org/library/datetime.html#strftime-strptime-behavior>`_
timestamp_format = string(default=None)
-# maximal length of authors string in search mode before it gets truncated
-authors_maxlength = integer(default=30)
-
# how to print messages:
# this specifies a shell command used for printing.
# threads/messages are piped to this command as plain text.
@@ -179,14 +176,10 @@ forward_subject_prefix = string(default='Fwd: ')
[tags]
# for each tag
[[__many__]]
- # foreground
- fg = string(default=None)
- # background
- bg = string(default=None)
- # foreground if focused
- focus_fg = string(default=None)
- # background if focused
- focus_bg = string(default=None)
+ # unfocussed
+ normal = attrtriple(default=None)
+ # focussed
+ focus = attrtriple(default=None)
# don't display at all?
hidden = boolean(default=False)
# alternative string representation
diff --git a/alot/defaults/default.theme b/alot/defaults/default.theme
index e5781912..e88f4a97 100644
--- a/alot/defaults/default.theme
+++ b/alot/defaults/default.theme
@@ -1,354 +1,87 @@
-[1]
-[[global]]
- [[[footer]]]
- fg = 'standout'
- [[[body]]]
- fg = 'default'
- [[[notify_error]]]
- fg = 'standout'
- [[[notify_normal]]]
- fg = 'default'
- [[[prompt]]]
- fg = 'default'
- [[[tag]]]
- fg = 'default'
- [[[tag_focus]]]
- fg = 'standout, bold'
-[[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'
-[[envelope]]
- [[[body]]]
- fg = 'default'
- [[[header]]]
- fg = 'default'
- [[[header_key]]]
- fg = 'default'
- [[[header_value]]]
- fg = 'default'
-[16]
-[[global]]
- [[[footer]]]
- bg = 'dark blue'
- fg = 'light green'
- [[[body]]]
- bg = 'default'
- fg = 'default'
- [[[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'
-[[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'
-[[envelope]]
- [[[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'
-[[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'
+[global]
+ footer = 'standout','','white,bold','dark blue','white,bold','#006'
+ body = 'default','','dark gray','default','g58','default'
+ notify_error = 'standout','','white','dark red','white','dark red'
+ notify_normal = 'default','','light gray','dark gray','light gray','#68a'
+ prompt = 'default','','light gray','black','light gray','g11'
+ tag = 'default','','light gray','black','light gray','default'
+ tag_focus = 'standout, bold','','white','dark gray','#ffa','#68a'
+[help]
+ text = 'default','','default','dark gray','default','g35'
+ section = 'underline','','bold,underline','dark gray','bold,underline','g35'
+ title = 'standout','','white','dark blue','white,bold,underline','g35'
+[bufferlist]
+ line_focus = 'standout','','yellow','light gray','#ff8','g58'
+ line_even = 'default','','light gray','black','default','g3'
+ line_odd = 'default','','light gray','black','default','default'
+[taglist]
+ line_focus = 'standout','','yellow','light gray','#ff8','g58'
+ line_even = 'default','','light gray','black','default','g3'
+ line_odd = 'default','','light gray','black','default','default'
+[thread]
+ arrow_heads = '','','dark red','','#a00',''
+ arrow_bars = '','','dark red','','#800',''
+ attachment = 'default','','light gray','dark gray','light gray','dark gray'
+ attachment_focus = 'underline','','light gray','light green','light gray','light green'
+ body = 'default','','light gray','default','light gray','default'
+ header = 'default','','white','dark gray','white','dark gray'
+ header_key = 'default','','white','dark gray','white','dark gray'
+ header_value = 'default','','light gray','dark gray','light gray','dark gray'
-[256]
-[[global]]
- # attributes used in all modi
- [[[footer]]]
- bg = '#006'
- fg = 'white'
- [[[body]]]
- bg = 'default'
- fg = 'default'
- [[[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'
-[[envelope]]
- [[[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 = 'default','','white','light blue','white','#006'
+ odd = 'default','','white','dark blue','white','#068'
+ focus = 'standout','','white','light gray','#ff8','g58'
+
+[envelope]
+ body = 'default','','light gray','default','light gray','default'
+ header = 'default','','white','dark gray','white','dark gray'
+ header_key = 'default','','white','dark gray','white','dark gray'
+ header_value = 'default','','light gray','dark gray','light gray','dark gray'
+[search]
+ [[threadline]]
+ normal = 'default','','default','default','#6d6','default'
+ focus = 'standout','','light gray','light gray','g85','g58'
+ parts = date,mailcount,tags,authors,subject
+ [[[date]]]
+ normal = 'default','','light gray','default','g74','default'
+ focus = 'standout','','yellow','light gray','yellow','g58'
+ width = 'fit',10,10
+ alignment = right
+ [[[mailcount]]]
+ normal = 'default','','light gray','default','g66','default'
+ focus = 'standout','','yellow','light gray','yellow','g58'
+ width = 'fit', 5,5
+ [[[tags]]]
+ normal = 'bold','','dark cyan','','dark cyan',''
+ focus = 'standout','','yellow','light gray','yellow','g58'
+ [[[authors]]]
+ normal = 'default,underline','','light blue','default','#068','default'
+ focus = 'standout','','yellow','light gray','yellow','g58'
+ width = 'fit',0,30
+ [[[subject]]]
+ normal = 'default','','light gray','default','g66','default'
+ focus = 'standout','','yellow','light gray','yellow','g58'
+ #width = 'weight', 1
+ [[[content]]]
+ normal = 'default','','light gray','default','dark gray','default'
+ focus = 'standout','','yellow','light gray','yellow','g58'
+ width = 'weight', 1
+
+ # highlight threads containing unread messages
+ [[threadline-unread]]
+ tagged_with = 'unread'
+ normal = 'default','','default,bold','default','#6d6,bold','default'
+ parts = date,mailcount,tags,authors,subject
+ [[[date]]]
+ normal = 'default','','light gray,bold','default','white','default'
+ [[[mailcount]]]
+ normal = 'default','','light gray,bold','default','g93','default'
+ [[[tags]]]
+ normal = 'bold','','dark cyan,bold','','#6dd',''
+ [[[authors]]]
+ normal = 'default,underline','','light blue,bold','default','#68f','default'
+ [[[subject]]]
+ normal = 'default','','light gray,bold','default','g93','default'
+ [[[content]]]
+ normal = 'default','','light gray,bold','default','dark gray,bold','default'
diff --git a/alot/defaults/theme.spec b/alot/defaults/theme.spec
index f23d91da..05d59c16 100644
--- a/alot/defaults/theme.spec
+++ b/alot/defaults/theme.spec
@@ -1,353 +1,65 @@
-[1]
-[[global]]
- [[[footer]]]
- fg = string(default='default')
- [[[body]]]
- 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')
-[[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')
-[[envelope]]
- [[[body]]]
- fg = string(default='default')
- [[[header]]]
- fg = string(default='default')
- [[[header_key]]]
- fg = string(default='default')
- [[[header_value]]]
- fg = string(default='default')
-
-[16]
-[[global]]
- [[[footer]]]
- bg = string(default='default')
- fg = string(default='default')
- [[[body]]]
- 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')
-[[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')
-[[envelope]]
- [[[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')
-[[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]]
+[global]
# attributes used in all modi
- [[[footer]]]
- bg = string(default=None)
- fg = string(default=None)
- [[[body]]]
- 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]]
+ footer = attrtriple
+ body = attrtriple
+ notify_error = attrtriple
+ notify_normal = attrtriple
+ prompt = attrtriple
+ tag = attrtriple
+ tag_focus = attrtriple
+[help]
# formatting 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)
+ text = attrtriple
+ section = attrtriple
+ title = attrtriple
# 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)
-[[envelope]]
- [[[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)
+[bufferlist]
+ line_focus = attrtriple
+ line_even = attrtriple
+ line_odd = attrtriple
+
+[taglist]
+ line_focus = attrtriple
+ line_even = attrtriple
+ line_odd = attrtriple
+[search]
+ [[threadline]]
+ normal = attrtriple
+ focus = attrtriple
+ # order subwidgets are displayed. subset of {date,mailcount,tags,authors,subject,count}
+ # every element listed must have its own subsection below
+ parts = string_list(default=None)
+ [[[__many__]]]
+ normal = attrtriple
+ focus = attrtriple
+ width = widthtuple(default=None)
+ alignment = align(default='left')
+ [[__many__]]
+ normal = attrtriple(default=None)
+ focus = attrtriple(default=None)
+ parts = string_list(default=None)
+ query = string(default=None)
+ tagged_with = force_list(default=None)
+ [[[__many__]]]
+ normal = attrtriple(default=None)
+ focus = attrtriple(default=None)
+ width = widthtuple(default=None)
+ alignment = align(default='right')
+[thread]
+ arrow_heads = attrtriple
+ arrow_bars = attrtriple
+ attachment = attrtriple
+ attachment_focus = attrtriple
+ body = attrtriple
+ header = attrtriple
+ header_key = attrtriple
+ header_value = attrtriple
+ [[summary]]
+ even = attrtriple
+ odd = attrtriple
+ focus = attrtriple
+[envelope]
+ body = attrtriple
+ header = attrtriple
+ header_key = attrtriple
+ header_value = attrtriple
diff --git a/alot/errors.py b/alot/errors.py
index 00336fc8..881acf1f 100644
--- a/alot/errors.py
+++ b/alot/errors.py
@@ -1,6 +1,8 @@
# 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
+
+
class GPGProblem(Exception):
"""GPG Error"""
pass
diff --git a/alot/helper.py b/alot/helper.py
index f4ec07cd..78744edf 100644
--- a/alot/helper.py
+++ b/alot/helper.py
@@ -22,7 +22,6 @@ from twisted.internet.protocol import ProcessProtocol
from twisted.internet.defer import Deferred
import StringIO
import logging
-import tempfile
def split_commandstring(cmdstring):
@@ -35,6 +34,7 @@ def split_commandstring(cmdstring):
cmdstring = cmdstring.encode('utf-8', errors='ignore')
return shlex.split(cmdstring)
+
def safely_get(clb, E, on_error=''):
"""
returns result of :func:`clb` and falls back to `on_error`
diff --git a/alot/settings/__init__.py b/alot/settings/__init__.py
index eaa8cd16..0edfc24c 100644
--- a/alot/settings/__init__.py
+++ b/alot/settings/__init__.py
@@ -7,9 +7,7 @@ import re
import errno
import mailcap
import logging
-import urwid
import shutil
-from urwid import AttrSpecError
from configobj import ConfigObj, Section
from alot.account import SendmailAccount
@@ -18,9 +16,12 @@ from alot.helper import pretty_datetime, string_decode
from errors import ConfigError
from utils import read_config
+from utils import resolve_att
from checks import force_list
from checks import mail_container
from checks import gpg_key
+from checks import attr_triple
+from checks import align_mode
from theme import Theme
@@ -62,6 +63,8 @@ class SettingsManager(object):
newconfig = read_config(path, spec,
checks={'mail_container': mail_container,
'force_list': force_list,
+ 'align': align_mode,
+ 'attrtriple': attr_triple,
'gpg_key_hint': gpg_key})
self._config.merge(newconfig)
@@ -90,7 +93,11 @@ class SettingsManager(object):
raise ConfigError(err_msg % (themestring, themes_dir))
else:
theme_path = os.path.join(themes_dir, themestring)
- self._theme = Theme(theme_path)
+ try:
+ self._theme = Theme(theme_path)
+ except ConfigError as e:
+ err_msg = 'Theme file %s failed validation:\n'
+ raise ConfigError((err_msg % themestring) + e.message)
self._accounts = self._parse_accounts(self._config)
self._accountmap = self._account_table(self._accounts)
@@ -213,7 +220,7 @@ class SettingsManager(object):
value = fallback
return value
- def get_theming_attribute(self, mode, name):
+ def get_theming_attribute(self, mode, name, part=None):
"""
looks up theming attribute
@@ -221,54 +228,85 @@ class SettingsManager(object):
:type mode: str
:param name: identifier of the atttribute
:type name: str
+ :rtype: urwid.AttrSpec
"""
colours = int(self._config.get('colourmode'))
- return self._theme.get_attribute(mode, name, colours)
+ return self._theme.get_attribute(colours, mode, name, part)
- def get_tagstring_representation(self, tag):
+ def get_threadline_theming(self, thread):
"""
- looks up user's preferred way to represent a given tagstring
+ looks up theming info a threadline displaying a given thread. This
+ wraps around :meth:`~alot.settings.theme.Theme.get_threadline_theming`,
+ filling in the current colour mode.
- This returns a dictionary mapping
- 'normal' and 'focussed' to `urwid.AttrSpec` sttributes,
- and 'translated' to an alternative string representation
+ :param thread: thread to theme
+ :type thread: alot.db.thread.Thread
"""
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
+ return self._theme.get_threadline_theming(thread, colours)
- hidden = self._config['tags'][sec]['hidden'] or False
+ def get_tagstring_representation(self, tag, onebelow_normal=None,
+ onebelow_focus=None):
+ """
+ looks up user's preferred way to represent a given tagstring.
- translated = self._config['tags'][sec]['translated'] or tag
- translation = self._config['tags'][sec]['translation']
+ :param tag: tagstring
+ :type tag: str
+ :param onebelow_normal: attribute that shines through if unfocussed
+ :type onebelow_normal: urwid.AttrSpec
+ :param onebelow_focus: attribute that shines through if focussed
+ :type onebelow_focus: urwid.AttrSpec
+
+ If `onebelow_normal` or `onebelow_focus` is given these attributes will
+ be used as fallbacks for fg/bg values '' and 'default'.
+
+ This returns a dictionary mapping
+ :normal: to :class:`urwid.AttrSpec` used if unfocussed
+ :focussed: to :class:`urwid.AttrSpec` used if focussed
+ :translated: to an alternative string representation
+ """
+ colourmode = int(self._config.get('colourmode'))
+ theme = self._theme
+ cfg = self._config
+ colours = [1, 16, 256]
+
+ def colourpick(triple):
+ """ pick attribute from triple (mono,16c,256c) according to current
+ colourmode"""
+ if triple is None:
+ return None
+ return triple[colours.index(colourmode)]
+
+ # global default attributes for tagstrings.
+ # These could contain values '' and 'default' which we interpret as
+ # "use the values from the widget below"
+ default_normal = theme.get_attribute(colourmode, 'global', 'tag')
+ default_focus = theme.get_attribute(colourmode, 'global', 'tag_focus')
+
+ # local defaults for tagstring attributes. depend on next lower widget
+ fallback_normal = resolve_att(onebelow_normal, default_normal)
+ fallback_focus = resolve_att(onebelow_focus, default_focus)
+
+ for sec in cfg['tags'].sections:
+ if re.match('^' + sec + '$', tag):
+ normal = resolve_att(colourpick(cfg['tags'][sec]['normal']),
+ fallback_normal)
+ focus = resolve_att(colourpick(cfg['tags'][sec]['focus']),
+ fallback_focus)
+
+ translated = cfg['tags'][sec]['translated']
+ if translated is None:
+ translated = tag
+ translation = cfg['tags'][sec]['translation']
if translation:
translated = re.sub(translation[0], translation[1], tag)
break
else:
- normal = default
- focussed = default_f
- hidden = False
+ normal = fallback_normal
+ focus = fallback_focus
translated = tag
- return {'normal': normal, 'focussed': focussed,
- 'hidden': hidden, 'translated': translated}
+ return {'normal': normal, 'focussed': focus, 'translated': translated}
def get_hook(self, key):
"""return hook (`callable`) identified by `key`"""
diff --git a/alot/settings/checks.py b/alot/settings/checks.py
index eab1253d..76d22780 100644
--- a/alot/settings/checks.py
+++ b/alot/settings/checks.py
@@ -3,15 +3,88 @@
# For further details see the COPYING file
import mailbox
import re
+from urwid import AttrSpec, AttrSpecError
from urlparse import urlparse
from validate import VdtTypeError
from validate import is_list
-from validate import ValidateError
+from validate import ValidateError, VdtValueTooLongError, VdtValueError
from alot import crypto
from alot.errors import GPGProblem
+def attr_triple(value):
+ """
+ Check that interprets the value as `urwid.AttrSpec` triple for the colour
+ modes 1,16 and 256. It assumes a <6 tuple of attribute strings for
+ mono foreground, mono background, 16c fg, 16c bg, 256 fg and 256 bg
+ respectively. If any of these are missing, we downgrade to the next
+ lower available pair, defaulting to 'default'.
+
+ :raises: VdtValueTooLongError, VdtTypeError
+ :rtype: triple of `urwid.AttrSpec`
+ """
+ keys = ['dfg', 'dbg', '1fg', '1bg', '16fg', '16bg', '256fg', '256bg']
+ acc = {}
+ if not isinstance(value, (list, tuple)):
+ value = value,
+ if len(value) > 6:
+ raise VdtValueTooLongError(value)
+ # ensure we have exactly 6 attribute strings
+ attrstrings = (value + (6 - len(value)) * [None])[:6]
+ # add fallbacks for the empty list
+ attrstrings = (2 * ['default']) + attrstrings
+ for i, value in enumerate(attrstrings):
+ if value:
+ acc[keys[i]] = value
+ else:
+ acc[keys[i]] = acc[keys[i - 2]]
+ try:
+ mono = AttrSpec(acc['1fg'], acc['1bg'], 1)
+ normal = AttrSpec(acc['16fg'], acc['16bg'], 16)
+ high = AttrSpec(acc['256fg'], acc['256bg'], 256)
+ except AttrSpecError, e:
+ raise ValidateError(e.message)
+ return mono, normal, high
+
+
+def align_mode(value):
+ """
+ test if value is one of 'left', 'right' or 'center'
+ """
+ if value not in ['left', 'right', 'center']:
+ raise VdtValueError
+ return value
+
+
+def width_tuple(value):
+ """
+ test if value is a valid width indicator (for a sub-widget in a column).
+ This can either be
+ ('fit', min, max): use the length actually needed for the content, padded
+ to use at least width min, and cut of at width max.
+ Here, min and max are positive integers or 0 to disable
+ the boundary.
+ ('weight',n): have it relative weight of n compared to other columns.
+ Here, n is an int.
+ """
+ if value is None:
+ res = 'fit', 0, 0
+ elif not isinstance(value, (list, tuple)):
+ raise VdtTypeError(value)
+ elif value[0] not in ['fit', 'weight']:
+ raise VdtTypeError(value)
+ if value[0] == 'fit':
+ if not isinstance(value[1], int) or not isinstance(value[2], int):
+ VdtTypeError(value)
+ res = 'fit', int(value[1]), int(value[2])
+ else:
+ if not isinstance(value[1], int):
+ VdtTypeError(value)
+ res = 'weight', int(value[1])
+ return res
+
+
def mail_container(value):
"""
Check that the value points to a valid mail container,
diff --git a/alot/settings/errors.py b/alot/settings/errors.py
index 9095e897..606f78e1 100644
--- a/alot/settings/errors.py
+++ b/alot/settings/errors.py
@@ -1,6 +1,8 @@
# 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
+
+
class ConfigError(Exception):
"""could not parse user config"""
pass
diff --git a/alot/settings/theme.py b/alot/settings/theme.py
index c6c36708..78f86fc5 100644
--- a/alot/settings/theme.py
+++ b/alot/settings/theme.py
@@ -2,9 +2,12 @@
# This file is released under the GNU GPL, version 3 or a later revision.
# For further details see the COPYING file
import os
-from urwid import AttrSpec, AttrSpecError
from utils import read_config
+from checks import align_mode
+from checks import attr_triple
+from checks import width_tuple
+from checks import force_list
from errors import ConfigError
DEFAULTSPATH = os.path.join(os.path.dirname(__file__), '..', 'defaults')
@@ -19,55 +22,110 @@ class Theme(object):
:raises: :class:`~alot.settings.errors.ConfigError`
"""
self._spec = os.path.join(DEFAULTSPATH, 'theme.spec')
- self._config = read_config(path, self._spec)
- self.attributes = self._parse_attributes(self._config)
+ self._config = read_config(path, self._spec,
+ checks={'align': align_mode,
+ 'widthtuple': width_tuple,
+ 'force_list': force_list,
+ 'attrtriple': 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 _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):
+ 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: identifier of the atttribute
+ :param name: of the atttribute
:type name: str
:param colourmode: colour mode; in [1, 16, 256]
:type colourmode: int
+ :rtype: urwid.AttrSpec
"""
- return self.attributes[colourmode][mode][name]
+ thmble = self._config[mode][name]
+ if part is not None:
+ thmble = thmble[part]
+ 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)
+
+ 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
diff --git a/alot/settings/utils.py b/alot/settings/utils.py
index 6667fa67..f6912f98 100644
--- a/alot/settings/utils.py
+++ b/alot/settings/utils.py
@@ -4,6 +4,7 @@
from configobj import ConfigObj, ConfigObjError, flatten_errors
from validate import Validator
from errors import ConfigError
+from urwid import AttrSpec
def read_config(configpath=None, specpath=None, checks={}):
@@ -23,22 +24,49 @@ def read_config(configpath=None, specpath=None, checks={}):
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))
+ except (ConfigObjError, IOError):
+ raise ConfigError('Couls not read %s' % configpath)
+ except UnboundLocalError:
+ # this works around a bug in configobj
+ msg = '%s is malformed. Check for sections without parents..'
+ raise ConfigError(msg % configpath)
if specpath:
validator = Validator()
validator.functions.update(checks)
- results = config.validate(validator)
+ try:
+ results = config.validate(validator, preserve_errors=True)
+ except ConfigObjError as e:
+ raise ConfigError(e.message)
+
if results != True:
- error_msg = 'Validation errors occurred:\n'
- for (section_list, key, _) in flatten_errors(config, results):
+ error_msg = ''
+ for (section_list, key, res) in flatten_errors(config, results):
if key is not None:
- msg = 'key "%s" in section "%s" failed validation'
- msg = msg % (key, ', '.join(section_list))
+ if res == False:
+ msg = 'key "%s" in section "%s" is missing.'
+ msg = msg % (key, ', '.join(section_list))
+ else:
+ msg = 'key "%s" in section "%s" failed validation: %s'
+ msg = msg % (key, ', '.join(section_list), res)
else:
- msg = 'section "%s" is malformed' % ', '.join(section_list)
+ msg = 'section "%s" is missing' % '.'.join(section_list)
error_msg += msg + '\n'
raise ConfigError(error_msg)
return config
+
+
+def resolve_att(a, fallback):
+ """ replace '' and 'default' by fallback values """
+ if a is None:
+ return fallback
+ if a.background in ['default', '']:
+ bg = fallback.background
+ else:
+ bg = a.background
+ if a.foreground in ['default', '']:
+ fg = fallback.foreground
+ else:
+ fg = a.foreground
+ return AttrSpec(fg, bg)
diff --git a/alot/ui.py b/alot/ui.py
index bb106cfc..ddb53798 100644
--- a/alot/ui.py
+++ b/alot/ui.py
@@ -4,7 +4,6 @@
import urwid
import logging
from twisted.internet import reactor, defer
-import sys
from settings import settings
from buffers import BufferlistBuffer
diff --git a/alot/widgets.py b/alot/widgets.py
index e084d0bf..16d4b53b 100644
--- a/alot/widgets.py
+++ b/alot/widgets.py
@@ -10,10 +10,21 @@ from alot.helper import tag_cmp
from alot.helper import string_decode
import alot.db.message as message
from alot.db.attachment import Attachment
-import time
from alot.db.utils import decode_header
+class AttrFlipWidget(urwid.AttrMap):
+ """
+ An AttrMap that can remember attributes to set
+ """
+ def __init__(self, w, maps, init_map='normal'):
+ self.maps = maps
+ urwid.AttrMap.__init__(self, w, maps[init_map])
+
+ def set_map(self, attrstring):
+ self.set_attr_map({None: self.maps[attrstring]})
+
+
class DialogBox(urwid.WidgetWrap):
def __init__(self, body, title, bodyattr=None, titleattr=None):
self.body = urwid.LineBox(body)
@@ -61,94 +72,86 @@ class ThreadlineWidget(urwid.AttrMap):
"""
selectable line widget that represents a :class:`~alot.db.Thread`
in the :class:`~alot.buffers.SearchBuffer`.
-
- Respected settings:
- * `general.display_content_in_threadline`
- * `general.timestamp_format`
- * `general.authors_maxlength`
- Theme settings:
- * `search_thread, search_thread_focus`
- * `search_thread_date, search_thread_date_focus`
- * `search_thread_mailcount, search_thread_mailcount_focus`
- * `search_thread_authors, search_thread_authors_focus`
- * `search_thread_subject, search_thread_subject_focus`
- * `search_thread_content, search_thread_content_focus`
"""
- # The pretty_datetime needs 9 characters, but only 8 if locale
- # doesn't use am/pm (in which case "jan 2012" is the longest)
- pretty_datetime_len = 8 if len(time.strftime("%P")) == 0 else 9
-
def __init__(self, tid, dbman):
self.dbman = dbman
- #logging.debug('tid: %s' % tid)
self.thread = dbman.get_thread(tid)
- #logging.debug('tid: %s' % self.thread)
self.tag_widgets = []
self.display_content = settings.get('display_content_in_threadline')
+ self.structure = None
self.rebuild()
- normal = settings.get_theming_attribute('search', 'thread')
- focussed = settings.get_theming_attribute('search', 'thread_focus')
+ normal = self.structure['normal']
+ focussed = self.structure['focus']
urwid.AttrMap.__init__(self, self.columns, normal, focussed)
- def rebuild(self):
- cols = []
- if self.thread:
- newest = self.thread.get_newest_date()
- else:
+ def _build_part(self, name, struct, minw, maxw, align):
+ def pad(string, shorten=None):
+ if maxw:
+ if len(string) > maxw:
+ if shorten:
+ string = shorten(string, maxw)
+ else:
+ string = string[:maxw]
+ if minw:
+ if len(string) < minw:
+ if align == 'left':
+ string = string.ljust(minw)
+ elif align == 'center':
+ string = string.center(minw)
+ else:
+ string = string.rjust(minw)
+ return string
+
+ part = None
+ width = None
+ if name == 'date':
newest = None
- if newest == None:
datestring = ''
- else:
- datestring = settings.represent_datetime(newest)
- datestring = datestring.rjust(self.pretty_datetime_len)
- self.date_w = urwid.AttrMap(urwid.Text(datestring),
- self._get_theme('date'))
- cols.append(('fixed', len(datestring), self.date_w))
-
- if self.thread:
- mailcountstring = "(%d)" % self.thread.get_total_messages()
- else:
- mailcountstring = "(?)"
- self.mailcount_w = urwid.AttrMap(urwid.Text(mailcountstring),
- self._get_theme('mailcount'))
- cols.append(('fixed', len(mailcountstring), self.mailcount_w))
-
- if self.thread:
- self.tag_widgets = [TagWidget(t)
- for t in self.thread.get_tags()]
- else:
- self.tag_widgets = []
- self.tag_widgets.sort(tag_cmp,
- lambda tag_widget: tag_widget.translated)
- for tag_widget in self.tag_widgets:
- if not tag_widget.hidden:
- cols.append(('fixed', tag_widget.width(), tag_widget))
+ if self.thread:
+ newest = self.thread.get_newest_date()
+ datestring = settings.represent_datetime(newest)
+ datestring = pad(datestring)
+ width = len(datestring)
+ part = AttrFlipWidget(urwid.Text(datestring), struct['date'])
- if self.thread:
- authors = self.thread.get_authors_string() or '(None)'
- else:
- authors = '(None)'
- maxlength = settings.get('authors_maxlength')
- authorsstring = shorten_author_string(authors, maxlength)
- self.authors_w = urwid.AttrMap(urwid.Text(authorsstring),
- self._get_theme('authors'))
- cols.append(('fixed', len(authorsstring), self.authors_w))
-
- if self.thread:
- subjectstring = self.thread.get_subject() or ''
- else:
- subjectstring = ''
- # sanitize subject string:
- subjectstring = subjectstring.replace('\n', ' ')
- subjectstring = subjectstring.replace('\r', '')
- subjectstring = subjectstring.strip()
-
- self.subject_w = urwid.AttrMap(urwid.Text(subjectstring, wrap='clip'),
- self._get_theme('subject'))
- if subjectstring:
- cols.append(('weight', 2, self.subject_w))
-
- if self.display_content:
+ elif name == 'mailcount':
+ if self.thread:
+ mailcountstring = "(%d)" % self.thread.get_total_messages()
+ else:
+ mailcountstring = "(?)"
+ datestring = pad(mailcountstring)
+ width = len(mailcountstring)
+ mailcount_w = AttrFlipWidget(urwid.Text(mailcountstring),
+ struct['mailcount'])
+ part = mailcount_w
+ elif name == 'authors':
+ if self.thread:
+ authors = self.thread.get_authors_string() or '(None)'
+ else:
+ authors = '(None)'
+ authorsstring = pad(authors, shorten_author_string)
+ authors_w = AttrFlipWidget(urwid.Text(authorsstring),
+ struct['authors'])
+ width = len(authorsstring)
+ part = authors_w
+
+ elif name == 'subject':
+ if self.thread:
+ subjectstring = self.thread.get_subject() or ' '
+ else:
+ subjectstring = ' '
+ # sanitize subject string:
+ subjectstring = subjectstring.replace('\n', ' ')
+ subjectstring = subjectstring.replace('\r', '')
+ subjectstring = pad(subjectstring)
+
+ subject_w = AttrFlipWidget(urwid.Text(subjectstring, wrap='clip'),
+ struct['subject'])
+ if subjectstring:
+ width = len(subjectstring)
+ part = subject_w
+
+ elif name == 'content':
if self.thread:
msgs = self.thread.get_messages().keys()
else:
@@ -156,39 +159,68 @@ class ThreadlineWidget(urwid.AttrMap):
# sort the most recent messages first
msgs.sort(key=lambda msg: msg.get_date(), reverse=True)
lastcontent = ' '.join([m.get_text_content() for m in msgs])
- contentstring = lastcontent.replace('\n', ' ').strip()
- self.content_w = urwid.AttrMap(urwid.Text(
+ contentstring = pad(lastcontent.replace('\n', ' ').strip())
+ content_w = AttrFlipWidget(urwid.Text(
contentstring,
wrap='clip'),
- self._get_theme('content'))
- cols.append(self.content_w)
+ struct['content'])
+ width = len(contentstring)
+ part = content_w
+ elif name == 'tags':
+ if self.thread:
+ fallback_normal = struct[name]['normal']
+ fallback_focus = struct[name]['focus']
+ tag_widgets = [TagWidget(t, fallback_normal, fallback_focus)
+ for t in self.thread.get_tags()]
+ tag_widgets.sort(tag_cmp,
+ lambda tag_widget: tag_widget.translated)
+ else:
+ tag_widgets = []
+ cols = []
+ length = -1
+ for tag_widget in tag_widgets:
+ if not tag_widget.hidden:
+ wrapped_tagwidget = tag_widget
+ tag_width = tag_widget.width()
+ cols.append(('fixed', tag_width, wrapped_tagwidget))
+ length += tag_width + 1
+ if cols:
+ part = urwid.Columns(cols, dividechars=1)
+ width = length
+ return width, part
- self.columns = urwid.Columns(cols, dividechars=1)
+ def rebuild(self):
+ self.widgets = []
+ columns = []
+ self.structure = settings.get_threadline_theming(self.thread)
+ for partname in self.structure['parts']:
+ minw = maxw = None
+ width_tuple = self.structure[partname]['width']
+ if width_tuple is not None:
+ if width_tuple[0] == 'fit':
+ minw, maxw = width_tuple[1:]
+ align_mode = self.structure[partname]['alignment']
+ width, part = self._build_part(partname, self.structure,
+ minw, maxw, align_mode)
+ if part is not None:
+ if isinstance(part, urwid.Columns):
+ for w in part.widget_list:
+ self.widgets.append(w)
+ else:
+ self.widgets.append(part)
+
+ # compute width and align
+ if width_tuple[0] == 'weight':
+ columnentry = width_tuple + (part,)
+ else:
+ columnentry = ('fixed', width, part)
+ columns.append(columnentry)
+ self.columns = urwid.Columns(columns, dividechars=1)
self.original_widget = self.columns
def render(self, size, focus=False):
- if focus:
- self.date_w.set_attr_map({None: self._get_theme('date', focus)})
- self.mailcount_w.set_attr_map({None:
- self._get_theme('mailcount', focus)})
- for tw in self.tag_widgets:
- tw.set_focussed()
- self.authors_w.set_attr_map({None: self._get_theme('authors',
- focus)})
- self.subject_w.set_attr_map({None: self._get_theme('subject',
- focus)})
- if self.display_content:
- self.content_w.set_attr_map(
- {None: self._get_theme('content', focus=True)})
- else:
- self.date_w.set_attr_map({None: self._get_theme('date')})
- self.mailcount_w.set_attr_map({None: self._get_theme('mailcount')})
- for tw in self.tag_widgets:
- tw.set_unfocussed()
- self.authors_w.set_attr_map({None: self._get_theme('authors')})
- self.subject_w.set_attr_map({None: self._get_theme('subject')})
- if self.display_content:
- self.content_w.set_attr_map({None: self._get_theme('content')})
+ for w in self.widgets:
+ w.set_map('focus' if focus else 'normal')
return urwid.AttrMap.render(self, size, focus)
def selectable(self):
@@ -201,10 +233,12 @@ class ThreadlineWidget(urwid.AttrMap):
return self.thread
def _get_theme(self, component, focus=False):
- attr_key = 'thread_{0}'.format(component)
+ path = ['search', 'threadline', component]
if focus:
- attr_key += '_focus'
- return settings.get_theming_attribute('search', attr_key)
+ path.append('focus')
+ else:
+ path.append('normal')
+ return settings.get_theming_attribute(path)
class BufferlineWidget(urwid.Text):
@@ -232,20 +266,24 @@ class TagWidget(urwid.AttrMap):
"""
text widget that renders a tagstring.
- It looks up the string it displays in the `tag-translate` section
- of the config as well as custom theme settings for its tag. The
- tag may also be configured as hidden, which users of this widget
- should honour.
+ It looks up the string it displays in the `tags` section
+ of the config as well as custom theme settings for its tag.
"""
- def __init__(self, tag):
+ def __init__(self, tag, fallback_normal=None, fallback_focus=None):
self.tag = tag
- representation = settings.get_tagstring_representation(tag)
- self.hidden = representation['hidden']
+ representation = settings.get_tagstring_representation(tag,
+ fallback_normal,
+ fallback_focus)
self.translated = representation['translated']
+ self.hidden = self.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)
+ normal_att = representation['normal']
+ focus_att = representation['focussed']
+ self.attmaps = {'normal': normal_att, 'focus': focus_att}
+ urwid.AttrMap.__init__(self, self.txt, normal_att, focus_att)
+
+ def set_map(self, attrstring):
+ self.set_attr_map({None: self.attmaps[attrstring]})
def width(self):
# evil voodoo hotfix for double width chars that may
@@ -262,10 +300,10 @@ class TagWidget(urwid.AttrMap):
return self.tag
def set_focussed(self):
- self.set_attr_map({None: self.focus_att})
+ self.set_attr_map(self.attmap['focus'])
def set_unfocussed(self):
- self.set_attr_map({None: self.normal_att})
+ self.set_attr_map(self.attmap['normal'])
class ChoiceWidget(urwid.Text):
@@ -361,9 +399,6 @@ class CompleteEdit(urwid.Edit):
class MessageWidget(urwid.WidgetWrap):
"""
Flow widget that renders a :class:`~alot.db.message.Message`.
-
- Respected settings:
- * `general.displayed_headers`
"""
#TODO: atm this is heavily bent to work nicely with ThreadBuffer to display
#a tree structure. A better way would be to keep this widget simple
@@ -410,6 +445,12 @@ class MessageWidget(urwid.WidgetWrap):
self._filtered_headers = [k for k in displayed if k in self.mail]
self._displayed_headers = None
+ bars = settings.get_theming_attribute('thread', 'arrow_bars')
+ self.arrow_bars_att = bars
+ heads = settings.get_theming_attribute('thread', 'arrow_heads')
+ self.arrow_heads_att = heads
+ logging.debug(self.arrow_heads_att)
+
self.rebuild() # this will build self.pile
urwid.WidgetWrap.__init__(self, self.pile)
@@ -444,12 +485,15 @@ class MessageWidget(urwid.WidgetWrap):
bc = list() # box_columns
if self.depth > 1:
bc.append(0)
- cols.append(self._get_spacer(self.bars_at[1:-1]))
+ spacer = self._get_spacer(self.bars_at[1:-1])
+ cols.append(spacer)
if self.depth > 0:
if self.bars_at[-1]:
- arrowhead = u'\u251c\u25b6'
+ arrowhead = [(self.arrow_bars_att, u'\u251c'),
+ (self.arrow_heads_att, u'\u25b6')]
else:
- arrowhead = u'\u2514\u25b6'
+ arrowhead = [(self.arrow_bars_att, u'\u2514'),
+ (self.arrow_heads_att, u'\u25b6')]
cols.append(('fixed', 2, urwid.Text(arrowhead)))
cols.append(self.sumw)
line = urwid.Columns(cols, box_columns=bc)
@@ -475,10 +519,10 @@ class MessageWidget(urwid.WidgetWrap):
lines = []
for key in self._displayed_headers:
if key in mail:
- if key.lower() in ['cc','bcc', 'to']:
+ if key.lower() in ['cc', 'bcc', 'to']:
values = mail.get_all(key)
- dvalues = [decode_header(v, normalize=norm) for v in values]
- lines.append((key, ', '.join(dvalues)))
+ values = [decode_header(v, normalize=norm) for v in values]
+ lines.append((key, ', '.join(values)))
else:
for value in mail.get_all(key):
dvalue = decode_header(value, normalize=norm)
@@ -548,6 +592,7 @@ class MessageWidget(urwid.WidgetWrap):
prefixchars.append(('fixed', 1, urwid.SolidFill(c)))
spacer = urwid.Columns(prefixchars, box_columns=range(length))
+ spacer = urwid.AttrMap(spacer, self.arrow_bars_att)
return ('fixed', length, spacer)
def _get_arrowhead_aligner(self):
@@ -555,7 +600,8 @@ class MessageWidget(urwid.WidgetWrap):
aligner = u'\u2502'
else:
aligner = ' '
- return ('fixed', 1, urwid.SolidFill(aligner))
+ aligner = urwid.SolidFill(aligner)
+ return ('fixed', 1, urwid.AttrMap(aligner, self.arrow_bars_att))
def selectable(self):
return True
@@ -575,11 +621,6 @@ class MessageWidget(urwid.WidgetWrap):
class MessageSummaryWidget(urwid.WidgetWrap):
"""
one line summary of a :class:`~alot.db.message.Message`.
-
- Theme settings:
- * `thread_summary_even`
- * `thread_summary_odd`
- * `thread_summary_focus`
"""
def __init__(self, message, even=True):
@@ -592,9 +633,11 @@ class MessageSummaryWidget(urwid.WidgetWrap):
self.message = message
self.even = even
if even:
- attr = settings.get_theming_attribute('thread', 'summary_even')
+ attr = settings.get_theming_attribute('thread', 'summary', 'even')
else:
- attr = settings.get_theming_attribute('thread', 'summary_odd')
+ attr = settings.get_theming_attribute('thread', 'summary', 'odd')
+ focus_att = settings.get_theming_attribute('thread', 'summary',
+ 'focus')
cols = []
sumstr = self.__str__()
@@ -603,12 +646,11 @@ class MessageSummaryWidget(urwid.WidgetWrap):
thread_tags = message.get_thread().get_tags(intersection=True)
outstanding_tags = set(message.get_tags()).difference(thread_tags)
- tag_widgets = [TagWidget(t) for t in outstanding_tags]
+ tag_widgets = [TagWidget(t, attr, focus_att) for t in outstanding_tags]
tag_widgets.sort(tag_cmp, lambda tag_widget: tag_widget.translated)
for tag_widget in tag_widgets:
if not tag_widget.hidden:
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,
focus_att)
urwid.WidgetWrap.__init__(self, line)
@@ -662,9 +704,6 @@ class HeadersList(urwid.WidgetWrap):
class MessageBodyWidget(urwid.AttrMap):
"""
displays printable parts of an email
-
- Theme settings:
- * `thread_body`
"""
def __init__(self, msg):
@@ -676,10 +715,6 @@ class MessageBodyWidget(urwid.AttrMap):
class AttachmentWidget(urwid.WidgetWrap):
"""
one-line summary of an :class:`~alot.db.attachment.Attachment`.
-
- Theme settings:
- * `thread_attachment`
- * `thread_attachment_focus`
"""
def __init__(self, attachment, selectable=True):
self._selectable = selectable
diff --git a/docs/source/configuration/index.rst b/docs/source/configuration/index.rst
index b14af2e1..b714133f 100644
--- a/docs/source/configuration/index.rst
+++ b/docs/source/configuration/index.rst
@@ -273,63 +273,142 @@ Alot can be run in 1, 16 or 256 colour mode. The requested mode is determined by
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.
+To specify the theme to use, set the :ref:`theme <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 :ref:`themes_dir <themes-dir>` config setting
which defaults to :file:`~/.config/alot/themes/`.
-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.
+.. _config.theming.themefiles:
+
+Theme Files
+-----------
+contain a section for each :ref:`MODE <modes>` plus "help" for the bindings-help overlay
+and "global" for globally used themables like footer, prompt etc.
+Each such section defines colour :ref:`attributes <config.theming.attributes>` for the parts that
+can be themed. 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 exact format.
+
+.. _config.theming.attributes:
+
+Colour Attributes
+-----------------
+Attributes are *sextuples* of `urwid Attribute strings <http://excess.org/urwid/wiki/DisplayAttributes>`__
+that specify foreground and background for mono, 16 and 256-colour modes respectively.
+For mono-mode only the flags `blink`, `standup`, `underline` and `bold` are available,
+16c mode supports these in combination with the colour names::
+
+ brown dark red dark magenta dark blue dark cyan dark green
+ yellow light red light magenta light blue light cyan light green
+ black dark gray light gray white
+
+In high-colour mode, you may use the above plus grayscales `g0` to `g100` and
+colour codes given as `#` followed by three hex values.
+See `here <http://excess.org/urwid/wiki/DisplayAttributes>`__
+and `here <http://excess.org/urwid/reference.html#AttrSpec>`__
+for more details on the interpreted values. A colour picker that makes choosing colours easy can be
+found in :file:`alot/extra/colour_picker.py`.
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'.
+ [[global]]
+ #name mono fg mono bg 16c fg 16c bg 256c fg 256c bg
+ # | | | | | |
+ # v v v v v v
+ footer = 'bold,underline', '', 'light red, bold, underline', 'light green', 'light red, bold, underline', '#8f6'
+
+Highlighting Thread lines in Search Mode
+----------------------------------------
+The subsection '[[threadline]]' of the '[search]' section in :ref:`Theme Files <config.theming.themefiles>`
+determines how to present a thread: here, :ref:`attributes <config.theming.attributes>` 'normal' and
+'focus' provide fallback/spacer themes and 'parts' is a (string) list of displayed subwidgets.
+Possible part strings are:
+
+* date
+* mailcount
+* tags
+* authors
+* subject
+
+For every listed part there must be a subsection with the same name, defining
+
+:normal: :ref:`attribute <config.theming.attributes>` used for this part if unfocussed
+:focus: :ref:`attribute <config.theming.attributes>` used for this part if focussed
+:width: tuple indicating the width of the part. 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: how to place the content string if the widget space is larger.
+ This must be one of 'right', 'left' or 'center'.
+
+To "highlight" some thread lines (use different attributes than the defaults found in the
+'[[threadline]]' section), one can define sections with prefix 'threadline'.
+Each one of those can redefine any part of the structure outlined above, the rest defaults to
+values defined in '[[threadline]]'.
+
+The section used to theme a particular thread is the first one (in file-order) that matches
+the criteria defined by its 'query' and 'taggeswith' values:
+
+* If 'query' is defined, the thread must match that querystring.
+* If 'tagged_with' is defined, is value (string list) must be a subset of the accumulated tags of all messages in the thread.
+
+.. note:: that 'tagged_with = A,B' is different from 'query = "is:A AND is:B"':
+ the latter will match only if the thread contains a single message that is both tagged with
+ A and B.
+
+ Moreover, note that if both query and tagged_with is undefined, this section will always match
+ and thus overwrite the defaults.
+
+The example below shows how to highlight unread threads:
+The date-part will be bold red if the thread has unread messages and flagged messages
+and just bold if the thread has unread but no flagged messages::
+
+ [search]
+ # default threadline
+ [[threadline]]
+ normal = 'default','default','default','default','#6d6','default'
+ focus = 'standout','default','light gray','dark gray','white','#68a'
+ parts = date,mailcount,tags,authors,subject
+ [[[date]]]
+ normal = 'default','default','light gray','default','g58','default'
+ focus = 'standout','default','light gray','dark gray','g89','#68a'
+ width = 'fit',10,10
+ ...
-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.
+ # highlight threads containing unread and flagged messages
+ [[threadline-flagged-unread]]
+ tagged_with = 'unread','flagged'
+ [[[date]]]
+ normal = 'default','default','light red,bold','default','light red,bold','default'
-.. _neat colour picker script: http://excess.org/urwid/browser/palette_test.py
+ # highlight threads containing unread messages
+ [[threadline-unread]]
+ query = 'is:unread'
+ [[[date]]]
+ normal = 'default','default','light gray,bold','default','g58,bold','default'
+.. _config.theming.tags:
Custom Tagstring Formatting
-===========================
+---------------------------
To specify how a particular tagstring is displayed 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 <themes>` section above:
+Such a section may define
-`fg` (foreground), `bg` (background), `focus_fg` (foreground if focused) and `focus_bg` (background if focused).
-An alternative string representation is read from the option `translated` or can be given
-as pair of strings in `translation`.
-
-The tag can also be hidden from view, if the key `hidden` is present and set to
-True. The tag can still be seen in the taglist buffer.
+:normal: :ref:`attribute <config.theming.attributes>` used if unfocussed
+:focus: :ref:`attribute <config.theming.attributes>` used if focussed
+:translated: fixed string representation for this tag. The tag can be hidden from view,
+ if the key `translated` is set to '', the empty string.
+:translation: a pair of strings that define a regular substitution to compute the string
+ representation on the fly using `re.sub`. This only really makes sense if
+ one uses a regular expression to match more than one tagstring (see below).
The following will make alot display the "todo" tag as "TODO" in white on red. ::
[tags]
[[todo]]
- bg = '#d66'
- fg = white
+ normal = '','', 'white','light red', 'white','#d66'
translated = TODO
Utf-8 symbols are welcome here, see e.g.
@@ -337,13 +416,14 @@ http://panmental.de/symbols/info.htm for some fancy symbols. I personally displa
like this::
[tags]
+
[[flagged]]
translated = ⚑
- fg = light red
+ normal = '','','light red','','light red',''
+ focus = '','','light red','','light red',''
[[unread]]
translated = ✉
- fg = white
[[replied]]
translated = ⏎
@@ -358,12 +438,11 @@ rename a matching tagstring. `translation` takes a comma separated *pair* of str
do the following::
[[notmuch::bug]]
- fg = 'light red, bold'
- bg = '#88d'
- translated = 'nm:bug'
+ translated = 'nm:bug'
+ normal = "", "", "light red, bold", "light blue", "light red, bold", "#88d"
+
[[notmuch::.*]]
- fg = '#fff'
- bg = '#88d'
- translation = 'notmuch::(.*)','nm:\1'
+ translation = 'notmuch::(.*)','nm:\1'
+ normal = "", "", "white", "light blue", "#fff", "#88d"
.. _nmbug: http://notmuchmail.org/nmbug/
diff --git a/extra/colour_picker.py b/extra/colour_picker.py
new file mode 100755
index 00000000..17fcf449
--- /dev/null
+++ b/extra/colour_picker.py
@@ -0,0 +1,259 @@
+#!/usr/bin/python
+#
+# COLOUR PICKER.
+# This is a lightly modified version of urwids palette_test.py example script as
+# found at https://raw.github.com/wardi/urwid/master/examples/palette_test.py
+#
+# This version simply omits resetting the screens default colour palette,
+# and therefore displays the colour attributes as alot would render them in
+# your terminal.
+#
+# Urwid Palette Test. Showing off highcolor support
+# Copyright (C) 2004-2009 Ian Ward
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+"""
+Palette test. Shows the available foreground and background settings
+in monochrome, 16 color, 88 color and 256 color modes.
+"""
+
+import re
+import sys
+
+import urwid
+import urwid.raw_display
+
+CHART_256 = """
+brown__ dark_red_ dark_magenta_ dark_blue_ dark_cyan_ dark_green_
+yellow_ light_red light_magenta light_blue light_cyan light_green
+
+ #00f#06f#08f#0af#0df#0ff black_______ dark_gray___
+ #60f#00d#06d#08d#0ad#0dd#0fd light_gray__ white_______
+ #80f#60d#00a#06a#08a#0aa#0da#0fa
+ #a0f#80d#60a#008#068#088#0a8#0d8#0f8
+ #d0f#a0d#80d#608#006#066#086#0a6#0d6#0f6
+ #f0f#d0d#a0a#808#606#000#060#080#0a0#0d0#0f0#0f6#0f8#0fa#0fd#0ff
+ #f0d#d0a#a08#806#600#660#680#6a0#6d0#6f0#6f6#6f8#6fa#6fd#6ff#0df
+ #f0a#d08#a06#800#860#880#8a0#8d0#8f0#8f6#8f8#8fa#8fd#8ff#6df#0af
+ #f08#d06#a00#a60#a80#aa0#ad0#af0#af6#af8#afa#afd#aff#8df#6af#08f
+ #f06#d00#d60#d80#da0#dd0#df0#df6#df8#dfa#dfd#dff#adf#8af#68f#06f
+ #f00#f60#f80#fa0#fd0#ff0#ff6#ff8#ffa#ffd#fff#ddf#aaf#88f#66f#00f
+ #fd0#fd6#fd8#fda#fdd#fdf#daf#a8f#86f#60f
+ #66d#68d#6ad#6dd #fa0#fa6#fa8#faa#fad#faf#d8f#a6f#80f
+ #86d#66a#68a#6aa#6da #f80#f86#f88#f8a#f8d#f8f#d6f#a0f
+ #a6d#86a#668#688#6a8#6d8 #f60#f66#f68#f6a#f6d#f6f#d0f
+#d6d#a6a#868#666#686#6a6#6d6#6d8#6da#6dd #f00#f06#f08#f0a#f0d#f0f
+ #d6a#a68#866#886#8a6#8d6#8d8#8da#8dd#6ad
+ #d68#a66#a86#aa6#ad6#ad8#ada#add#8ad#68d
+ #d66#d86#da6#dd6#dd8#dda#ddd#aad#88d#66d g78_g82_g85_g89_g93_g100
+ #da6#da8#daa#dad#a8d#86d g52_g58_g62_g66_g70_g74_
+ #88a#8aa #d86#d88#d8a#d8d#a6d g27_g31_g35_g38_g42_g46_g50_
+ #a8a#888#8a8#8aa #d66#d68#d6a#d6d g0__g3__g7__g11_g15_g19_g23_
+ #a88#aa8#aaa#88a
+ #a88#a8a
+"""
+
+CHART_88 = """
+brown__ dark_red_ dark_magenta_ dark_blue_ dark_cyan_ dark_green_
+yellow_ light_red light_magenta light_blue light_cyan light_green
+
+ #00f#08f#0cf#0ff black_______ dark_gray___
+ #80f#00c#08c#0cc#0fc light_gray__ white_______
+ #c0f#80c#008#088#0c8#0f8
+#f0f#c0c#808#000#080#0c0#0f0#0f8#0fc#0ff #88c#8cc
+ #f0c#c08#800#880#8c0#8f0#8f8#8fc#8ff#0cf #c8c#888#8c8#8cc
+ #f08#c00#c80#cc0#cf0#cf8#cfc#cff#8cf#08f #c88#cc8#ccc#88c
+ #f00#f80#fc0#ff0#ff8#ffc#fff#ccf#88f#00f #c88#c8c
+ #fc0#fc8#fcc#fcf#c8f#80f
+ #f80#f88#f8c#f8f#c0f g62_g74_g82_g89_g100
+ #f00#f08#f0c#f0f g0__g19_g35_g46_g52
+"""
+
+CHART_16 = """
+brown__ dark_red_ dark_magenta_ dark_blue_ dark_cyan_ dark_green_
+yellow_ light_red light_magenta light_blue light_cyan light_green
+
+black_______ dark_gray___ light_gray__ white_______
+"""
+
+ATTR_RE = re.compile("(?P<whitespace>[ \n]*)(?P<entry>[^ \n]+)")
+SHORT_ATTR = 4 # length of short high-colour descriptions which may
+# be packed one after the next
+
+def parse_chart(chart, convert):
+ """
+ Convert string chart into text markup with the correct attributes.
+
+ chart -- palette chart as a string
+ convert -- function that converts a single palette entry to an
+ (attr, text) tuple, or None if no match is found
+ """
+ out = []
+ for match in re.finditer(ATTR_RE, chart):
+ if match.group('whitespace'):
+ out.append(match.group('whitespace'))
+ entry = match.group('entry')
+ entry = entry.replace("_", " ")
+ while entry:
+ # try the first four characters
+ attrtext = convert(entry[:SHORT_ATTR])
+ if attrtext:
+ elen = SHORT_ATTR
+ entry = entry[SHORT_ATTR:].strip()
+ else: # try the whole thing
+ attrtext = convert(entry.strip())
+ assert attrtext, "Invalid palette entry: %r" % entry
+ elen = len(entry)
+ entry = ""
+ attr, text = attrtext
+ out.append((attr, text.ljust(elen)))
+ return out
+
+def foreground_chart(chart, background, colors):
+ """
+ Create text markup for a foreground colour chart
+
+ chart -- palette chart as string
+ background -- colour to use for background of chart
+ colors -- number of colors (88 or 256)
+ """
+ def convert_foreground(entry):
+ try:
+ attr = urwid.AttrSpec(entry, background, colors)
+ except urwid.AttrSpecError:
+ return None
+ return attr, entry
+ return parse_chart(chart, convert_foreground)
+
+def background_chart(chart, foreground, colors):
+ """
+ Create text markup for a background colour chart
+
+ chart -- palette chart as string
+ foreground -- colour to use for foreground of chart
+ colors -- number of colors (88 or 256)
+
+ This will remap 8 <= colour < 16 to high-colour versions
+ in the hopes of greater compatibility
+ """
+ def convert_background(entry):
+ try:
+ attr = urwid.AttrSpec(foreground, entry, colors)
+ except urwid.AttrSpecError:
+ return None
+ # fix 8 <= colour < 16
+ if colors > 16 and attr.background_basic and \
+ attr.background_number >= 8:
+ # use high-colour with same number
+ entry = 'h%d'%attr.background_number
+ attr = urwid.AttrSpec(foreground, entry, colors)
+ return attr, entry
+ return parse_chart(chart, convert_background)
+
+
+def main():
+ palette = [
+ ('header', 'black,underline', 'light gray', 'standout,underline',
+ 'black,underline', '#88a'),
+ ('panel', 'light gray', 'dark blue', '',
+ '#ffd', '#00a'),
+ ('focus', 'light gray', 'dark cyan', 'standout',
+ '#ff8', '#806'),
+ ]
+
+ screen = urwid.raw_display.Screen()
+ screen.register_palette(palette)
+
+ lb = urwid.SimpleListWalker([])
+ chart_offset = None # offset of chart in lb list
+
+ mode_radio_buttons = []
+ chart_radio_buttons = []
+
+ def fcs(widget):
+ # wrap widgets that can take focus
+ return urwid.AttrMap(widget, None, 'focus')
+
+ def set_mode(colors, is_foreground_chart):
+ # set terminal mode and redraw chart
+ screen.set_terminal_properties(colors)
+
+ chart_fn = (background_chart, foreground_chart)[is_foreground_chart]
+ if colors == 1:
+ lb[chart_offset] = urwid.Divider()
+ else:
+ chart = {16: CHART_16, 88: CHART_88, 256: CHART_256}[colors]
+ txt = chart_fn(chart, 'default', colors)
+ lb[chart_offset] = urwid.Text(txt, wrap='clip')
+
+ def on_mode_change(rb, state, colors):
+ # if this radio button is checked
+ if state:
+ is_foreground_chart = chart_radio_buttons[0].state
+ set_mode(colors, is_foreground_chart)
+
+ def mode_rb(text, colors, state=False):
+ # mode radio buttons
+ rb = urwid.RadioButton(mode_radio_buttons, text, state)
+ urwid.connect_signal(rb, 'change', on_mode_change, colors)
+ return fcs(rb)
+
+ def on_chart_change(rb, state):
+ # handle foreground check box state change
+ set_mode(screen.colors, state)
+
+ def click_exit(button):
+ raise urwid.ExitMainLoop()
+
+ lb.extend([
+ urwid.AttrMap(urwid.Text("Urwid Palette Test"), 'header'),
+ urwid.AttrMap(urwid.Columns([
+ urwid.Pile([
+ mode_rb("Monochrome", 1),
+ mode_rb("16-Color", 16, True),
+ mode_rb("88-Color", 88),
+ mode_rb("256-Color", 256),]),
+ urwid.Pile([
+ fcs(urwid.RadioButton(chart_radio_buttons,
+ "Foreground Colors", True, on_chart_change)),
+ fcs(urwid.RadioButton(chart_radio_buttons,
+ "Background Colors")),
+ urwid.Divider(),
+ fcs(urwid.Button("Exit", click_exit)),
+ ]),
+ ]),'panel')
+ ])
+
+ chart_offset = len(lb)
+ lb.extend([
+ urwid.Divider() # placeholder for the chart
+ ])
+
+ set_mode(16, True) # displays the chart
+
+ def unhandled_input(key):
+ if key in ('Q','q','esc'):
+ raise urwid.ExitMainLoop()
+
+ urwid.MainLoop(urwid.ListBox(lb), screen=screen,
+ unhandled_input=unhandled_input).run()
+
+if __name__ == "__main__":
+ main()
+
+
diff --git a/extra/tagsections_convert.py b/extra/tagsections_convert.py
new file mode 100755
index 00000000..3a73f60f
--- /dev/null
+++ b/extra/tagsections_convert.py
@@ -0,0 +1,77 @@
+#!/usr/bin/python
+"""
+ CONFIG CONVERTER
+ this script converts your custom tag string section from the v.3.1 syntax
+ to the current format.
+
+ >>> tagsections_convert.py -o config.new config.old
+
+ will convert your whole alot config safely to the new format.
+"""
+
+from configobj import ConfigObj
+import argparse
+import sys
+import re
+
+
+def get_leaf_value(cfg, path, fallback=''):
+ if len(path) == 1:
+ if isinstance(cfg, ConfigObj):
+ if path[0] not in cfg.scalars:
+ return fallback
+ else:
+ return cfg[path[0]]
+ else:
+ if path[0] not in cfg:
+ return fallback
+ else:
+ return cfg[path[0]]
+ else:
+ if path[0] in cfg:
+ scfg = cfg[path[0]]
+ sp = path[1:]
+ return get_leaf_value(scfg, sp, fallback)
+ else:
+ return None
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description='update alot theme files')
+ parser.add_argument('configfile', type=argparse.FileType('r'),
+ help='theme file to convert')
+ parser.add_argument('-o', type=argparse.FileType('w'), dest='out',
+ help='destination', default=sys.stdout)
+ args = parser.parse_args()
+
+ cfg = ConfigObj(args.configfile)
+ out = args.out
+ print args
+
+ def is_256(att):
+ r = r'(g\d{1,3}(?!\d))|(#[0-9A-Fa-f]{3}(?![0-9A-Fa-f]))'
+ return re.search(r, att)
+
+ if 'tags' in cfg:
+ for tag in cfg['tags'].sections:
+ sec = cfg['tags'][tag]
+ att = [''] * 6
+
+ if 'fg' in sec:
+ fg = sec['fg']
+ if not is_256(fg):
+ att[2] = fg
+ att[4] = fg
+ del(sec['fg'])
+
+ if 'bg' in sec:
+ bg = sec['bg']
+ if not is_256(bg):
+ att[3] = bg
+ att[5] = bg
+ del(sec['bg'])
+ sec['normal'] = att
+
+ if sec.get('hidden'):
+ sec['translated'] = ''
+ cfg.write(out)
diff --git a/extra/theme_convert.py b/extra/theme_convert.py
new file mode 100755
index 00000000..20113be0
--- /dev/null
+++ b/extra/theme_convert.py
@@ -0,0 +1,124 @@
+#!/usr/bin/python
+"""
+ THEME CONVERTER
+ this script converts your custom alot theme files from the v.3.1 syntax
+ to the current format.
+
+ >>> theme_convert.py -o themefile.new themefile.old
+"""
+
+from configobj import ConfigObj
+import argparse
+import sys
+
+
+def get_leaf_value(cfg, path, fallback=''):
+ if len(path) == 1:
+ if isinstance(cfg, ConfigObj):
+ if path[0] not in cfg.scalars:
+ return fallback
+ else:
+ return cfg[path[0]]
+ else:
+ if path[0] not in cfg:
+ return fallback
+ else:
+ return cfg[path[0]]
+ else:
+ if path[0] in cfg:
+ scfg = cfg[path[0]]
+ sp = path[1:]
+ return get_leaf_value(scfg, sp, fallback)
+ else:
+ return None
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description='update alot theme files')
+ parser.add_argument('themefile', type=argparse.FileType('r'),
+ help='theme file to convert')
+ parser.add_argument('-o', type=argparse.FileType('w'), dest='out',
+ help='destination', default=sys.stdout)
+ args = parser.parse_args()
+
+ old = ConfigObj(args.themefile)
+ new = ConfigObj()
+ out = args.out
+
+ def lookup(path):
+ values = []
+ for c in ['1', '16', '256']:
+ values.append(get_leaf_value(old, [c] + path + ['fg']) or 'default')
+ values.append(get_leaf_value(old, [c] + path + ['bg']) or 'default')
+ return values
+ values = map(lambda s: '\'' + s + '\'', values)
+ return ','.join(values)
+
+ for bmode in ['global', 'help', 'envelope']:
+ new[bmode] = {}
+ #out.write('[%s]\n' % bmode)
+ for themable in old['16'][bmode].sections:
+ new[bmode][themable] = lookup([bmode, themable])
+ #out.write(' %s = %s\n' % (themable, lookup([bmode, themable])))
+
+ # BUFFERLIST
+ new['bufferlist'] = {}
+ new['bufferlist']['line_even'] = lookup(['bufferlist','results_even'])
+ new['bufferlist']['line_odd'] = lookup(['bufferlist','results_odd'])
+ new['bufferlist']['line_focus'] = lookup(['bufferlist','focus'])
+
+ # TAGLIST
+ new['taglist'] = {}
+ new['taglist']['line_even'] = lookup(['bufferlist','results_even'])
+ new['taglist']['line_odd'] = lookup(['bufferlist','results_odd'])
+ new['taglist']['line_focus'] = lookup(['bufferlist','focus'])
+
+ # SEARCH
+ new['search'] = {}
+
+ new['search']['threadline'] = {}
+ new['search']['threadline']['normal'] = lookup(['search', 'thread'])
+ new['search']['threadline']['focus'] = lookup(['search', 'thread_focus'])
+ new['search']['threadline']['parts'] = ['date','mailcount','tags','authors','subject']
+
+ new['search']['threadline']['date'] = {}
+ new['search']['threadline']['date']['normal'] = lookup(['search', 'thread_date'])
+ new['search']['threadline']['date']['focus'] = lookup(['search', 'thread_date_focus'])
+
+ new['search']['threadline']['mailcount'] = {}
+ new['search']['threadline']['mailcount']['normal'] = lookup(['search', 'thread_mailcount'])
+ new['search']['threadline']['mailcount']['focus'] = lookup(['search', 'thread_mailcount_focus'])
+
+ new['search']['threadline']['tags'] = {}
+ new['search']['threadline']['tags']['normal'] = lookup(['search', 'thread_tags'])
+ new['search']['threadline']['tags']['focus'] = lookup(['search', 'thread_tags_focus'])
+
+ new['search']['threadline']['authors'] = {}
+ new['search']['threadline']['authors']['normal'] = lookup(['search', 'thread_authors'])
+ new['search']['threadline']['authors']['focus'] = lookup(['search', 'thread_authors_focus'])
+
+ new['search']['threadline']['subject'] = {}
+ new['search']['threadline']['subject']['normal'] = lookup(['search', 'thread_subject'])
+ new['search']['threadline']['subject']['focus'] = lookup(['search', 'thread_subject_focus'])
+
+ new['search']['threadline']['content'] = {}
+ new['search']['threadline']['content']['normal'] = lookup(['search', 'thread_content'])
+ new['search']['threadline']['content']['focus'] = lookup(['search', 'thread_content_focus'])
+
+ # THREAD
+ new['thread'] = {}
+ new['thread']['attachment'] = lookup(['thread','attachment'])
+ new['thread']['attachment_focus'] = lookup(['thread','attachment_focus'])
+ new['thread']['body'] = lookup(['thread','body'])
+ new['thread']['arrow_heads'] = lookup(['thread','body'])
+ new['thread']['arrow_bars'] = lookup(['thread','body'])
+ new['thread']['header'] = lookup(['thread','header'])
+ new['thread']['header_key'] = lookup(['thread','header_key'])
+ new['thread']['header_value'] = lookup(['thread','header_value'])
+ new['thread']['summary'] = {}
+ new['thread']['summary']['even'] = lookup(['thread','summary_even'])
+ new['thread']['summary']['odd'] = lookup(['thread','summary_odd'])
+ new['thread']['summary']['focus'] = lookup(['thread','summary_focus'])
+
+ # write out
+ new.write(out)
diff --git a/extra/themes/mutt b/extra/themes/mutt
new file mode 100644
index 00000000..2a6c6b09
--- /dev/null
+++ b/extra/themes/mutt
@@ -0,0 +1,76 @@
+###############################################################################
+# MUTT
+#
+# colour theme for alot. © 2012 Patrick Totzke, GNU GPL3+
+# https://github.com/pazz/alot
+###############################################################################
+
+[global]
+ footer = 'standout,bold','','light green,bold','dark blue','light green,bold','dark blue'
+ body = '','','light gray','black','light gray','black'
+ notify_error = 'standout','','light gray','dark red','light gray','dark red'
+ notify_normal = '','','light gray','black','light gray','#68a'
+ prompt = '','','light gray','black','light gray','black'
+ tag = '','','yellow','','yellow',''
+ tag_focus = 'standout, bold','','yellow','','yellow',''
+[help]
+ text = '','','light gray','dark gray','light gray','dark gray'
+ section = 'underline','','white,underline','dark gray','white,underline','dark gray'
+ title = 'standout','','white,underline','dark gray','white,underline','dark gray'
+[bufferlist]
+ line_even = '','','light gray','black','light gray','black'
+ line_odd = '','','light gray','black','light gray','black'
+ line_focus = 'standout','','black','dark cyan','black','dark cyan'
+[taglist]
+ line_even = '','','light gray','black','light gray','black'
+ line_odd = '','','light gray','black','light gray','black'
+ line_focus = 'standout','','black','dark cyan','black','dark cyan'
+[thread]
+ arrow_heads = '','','dark red','black','dark red','black'
+ arrow_bars = '','','dark red','black','dark red','black'
+ attachment = '','','yellow,bold','black','yellow,bold','black'
+ attachment_focus = 'standout','','black','yellow','black','yellow'
+ body = '','','light gray','black','light gray','black'
+ header = '','','dark cyan','black','dark cyan','black'
+ header_key = '','','dark cyan','black','dark cyan','black'
+ header_value = '','','dark cyan','black','dark cyan','black'
+
+ [[summary]]
+ even = '','','light gray','black','light gray','black'
+ odd = '','','light gray','black','light gray','black'
+ focus = 'standout','','black','dark cyan','black','dark cyan'
+
+[envelope]
+ body = '','','light gray','black','light gray','black'
+ header = '','','dark cyan','black','dark cyan','black'
+ header_key = '','','dark cyan','black','dark cyan','black'
+ header_value = '','','dark cyan','black','dark cyan','black'
+[search]
+ [[threadline]]
+ normal = '','','light gray','black','light gray','black'
+ focus = 'standout','','black','dark cyan','black','dark cyan'
+ parts = date,authors,mailcount,subject,tags
+ [[[date]]]
+ normal = '','','light gray','black','light gray','black'
+ focus = 'standout','','black','dark cyan','black','dark cyan'
+ width = 'fit',10,10
+ alignment = right
+ [[[mailcount]]]
+ normal = '','','light gray','black','light gray','black'
+ focus = 'standout','','black','dark cyan','black','dark cyan'
+ width = 'fit', 5,5
+ [[[tags]]]
+ normal = '','','yellow','black','yellow','black'
+ focus = 'standout','','black','dark cyan','black','dark cyan'
+ [[[authors]]]
+ normal = '','','light gray','black','light gray','black'
+ focus = 'standout','','black','dark cyan','black','dark cyan'
+ width = 'fit',25,25
+ [[[subject]]]
+ normal = '','','light gray','black','light gray','black'
+ focus = 'standout','','black','dark cyan','black','dark cyan'
+ width = 'weight', 1
+ [[[content]]]
+ normal = '','','light gray','black','light gray','black'
+ focus = 'standout','','black','dark cyan','black','dark cyan'
+ width = 'weight', 1
diff --git a/extra/themes/screenshots/mutt.search.png b/extra/themes/screenshots/mutt.search.png
new file mode 100644
index 00000000..517061e1
--- /dev/null
+++ b/extra/themes/screenshots/mutt.search.png
Binary files differ
diff --git a/extra/themes/screenshots/mutt.thread.png b/extra/themes/screenshots/mutt.thread.png
new file mode 100644
index 00000000..39b6566c
--- /dev/null
+++ b/extra/themes/screenshots/mutt.thread.png
Binary files differ
diff --git a/extra/themes/screenshots/solarized.search.png b/extra/themes/screenshots/solarized.search.png
new file mode 100644
index 00000000..ea78e9ff
--- /dev/null
+++ b/extra/themes/screenshots/solarized.search.png
Binary files differ
diff --git a/extra/themes/screenshots/solarized.thread.png b/extra/themes/screenshots/solarized.thread.png
new file mode 100644
index 00000000..4a0fe79a
--- /dev/null
+++ b/extra/themes/screenshots/solarized.thread.png
Binary files differ
diff --git a/extra/themes/screenshots/solarized_dark.search.png b/extra/themes/screenshots/solarized_dark.search.png
new file mode 100644
index 00000000..33b71ea2
--- /dev/null
+++ b/extra/themes/screenshots/solarized_dark.search.png
Binary files differ
diff --git a/extra/themes/screenshots/solarized_dark.thread.png b/extra/themes/screenshots/solarized_dark.thread.png
new file mode 100644
index 00000000..03548cac
--- /dev/null
+++ b/extra/themes/screenshots/solarized_dark.thread.png
Binary files differ
diff --git a/extra/themes/solarized b/extra/themes/solarized
new file mode 100644
index 00000000..11030e1c
--- /dev/null
+++ b/extra/themes/solarized
@@ -0,0 +1,126 @@
+###############################################################################
+# SOLARIZED DARK
+#
+# colour theme for alot. © 2012 Patrick Totzke, GNU GPL3+
+# http://ethanschoonover.com/solarized
+# https://github.com/pazz/alot
+###############################################################################
+#
+# Define mappings from solarized colour names to urwid attribute names for 16
+# and 256 colour modes. These work well assuming you use the solarized term
+# colours via Xressources/Xdefaults. You might want to change this otherwise
+
+16_base03 = 'dark gray'
+16_base02 = 'black'
+16_base01 = 'light green'
+16_base00 = 'yellow'
+16_base0 = 'light blue'
+16_base1 = 'light cyan'
+16_base2 = 'light gray'
+16_base3 = 'white'
+16_yellow = 'brown'
+16_orange = 'light red'
+16_red = 'dark red'
+16_magenta = 'dark magenta'
+16_violet = 'light magenta'
+16_blue = 'dark blue'
+16_cyan = 'dark cyan'
+16_green = 'dark green'
+
+# Use a slightly different mapping here to be able to use "bold" in 256c mode
+256_base03 = 'dark gray'
+256_base02 = 'black'
+256_base01 = 'light green'
+256_base00 = 'yellow'
+256_base0 = 'g66'
+256_base1 = 'g70'
+256_base2 = 'light gray'
+256_base3 = 'white'
+256_yellow = 'brown'
+256_orange = 'light red'
+256_red = 'dark red'
+256_magenta = 'dark magenta'
+256_violet = 'light magenta'
+256_blue = 'dark blue'
+256_cyan = '#088'
+256_green = 'dark green'
+
+# This is the actual alot theme
+[global]
+ footer = 'standout','default','%(16_base01)s','%(16_base2)s','%(256_base01)s','%(256_base2)s'
+ body = 'default','default','%(16_base00)s','%(16_base3)s','%(256_base00)s','%(256_base3)s'
+ notify_error = 'standout','default','%(16_base3)s','%(16_red)s','%(256_base3)s','%(256_red)s'
+ notify_normal = 'default','default','%(16_base00)s','%(16_base2)s','%(256_base00)s','%(256_base2)s'
+ prompt = 'default','default','%(16_base00)s','%(16_base2)s','%(256_base00)s','%(256_base2)s'
+ tag = 'default','default','%(16_yellow)s','%(16_base3)s','%(256_yellow)s','%(256_base3)s'
+ tag_focus = 'standout','default','%(16_base3)s','%(16_yellow)s','%(256_base3)s','%(256_yellow)s'
+[help]
+ text = 'default','default','light gray','dark gray','%(256_base00)s','%(256_base3)s'
+ section = 'underline','default','%(16_base01)s','%(16_base2)s','%(256_base01)s','%(256_base2)s'
+ title = 'standout','default','%(16_base01)s','%(16_base3)s','%(256_base01)s','%(256_base3)s'
+[taglist]
+ line_focus = 'standout','default','%(16_base2)s','%(16_yellow)s','%(256_base2)s','%(256_yellow)s'
+ line_even = 'default','default','%(16_base00)s','%(16_base3)s','%(256_base00)s','%(256_base3)s'
+ line_odd = 'default','default','%(16_base00)s','%(16_base2)s','%(256_base00)s','%(256_base2)s'
+[bufferlist]
+ line_focus = 'standout','default','%(16_base2)s','%(16_yellow)s','%(256_base2)s','%(256_yellow)s'
+ line_even = 'default','default','%(16_base00)s','%(16_base3)s','%(256_base00)s','%(256_base3)s'
+ line_odd = 'default','default','%(16_base00)s','%(16_base2)s','%(256_base00)s','%(256_base2)s'
+[thread]
+ attachment = 'default','default','%(16_base00)s','%(16_base3)s','%(256_base00)s','%(256_base3)s'
+ attachment_focus = 'underline','default','%(16_base2)s','%(16_yellow)s','%(256_base2)s','%(256_yellow)s'
+ body = 'default','default','%(16_base00)s','%(16_base3)s','%(256_base00)s','%(256_base3)s'
+ arrow_bars = 'default','default','%(16_yellow)s','%(16_base3)s','%(256_yellow)s','%(256_base3)s'
+ arrow_heads = 'default','default','%(16_yellow)s','%(16_base3)s','%(256_yellow)s','%(256_base3)s'
+ header = 'default','default','%(16_base00)s','%(16_base2)s','%(256_base00)s','%(256_base2)s'
+ header_key = 'default','default','%(16_magenta)s','%(16_base2)s','%(256_magenta)s','%(256_base2)s'
+ header_value = 'default','default','%(16_blue)s','%(16_base2)s','%(256_blue)s','%(256_base2)s'
+ [[summary]]
+ even = 'default','default','%(16_base00)s','%(16_base2)s','%(256_base00)s','%(256_base2)s'
+ focus = 'standout','default','%(16_base3)s','%(16_yellow)s','%(256_base3)s','%(256_yellow)s'
+ odd = 'default','default','%(16_base00)s','%(16_base3)s','%(256_base00)s','%(256_base3)s'
+[envelope]
+ body = 'default','default','%(16_base00)s','%(16_base3)s','%(256_base00)s','%(256_base3)s'
+ header = 'default','default','%(16_base00)s','%(16_base2)s','%(256_base00)s','%(256_base2)s'
+ header_key = 'default','default','%(16_orange)s','%(16_base2)s','%(256_orange)s','%(256_base2)s'
+ header_value = 'default','default','%(16_violet)s','%(16_base2)s','%(256_violet)s','%(256_base2)s'
+[search]
+ [[threadline]]
+ normal = 'default','default','%(16_base01)s','%(16_base3)s','%(256_base01)s','%(256_base3)s'
+ focus = 'standout','default','%(16_base2)s','%(16_yellow)s','%(256_base2)s','%(256_yellow)s'
+ parts = date,mailcount,tags,authors,subject
+ [[[date]]]
+ normal = 'default','default','%(16_base01)s','%(16_base3)s','%(256_base01)s','%(256_base3)s'
+ focus = 'standout','default','%(16_base3)s','%(16_yellow)s','%(256_base3)s','%(256_yellow)s'
+ [[[mailcount]]]
+ normal = 'default','default','%(16_base01)s','%(16_base3)s','%(256_base01)s','%(256_base3)s'
+ focus = 'standout','default','%(16_base2)s','%(16_yellow)s','%(256_base2)s','%(256_yellow)s'
+ [[[tags]]]
+ normal = 'bold','default','%(16_yellow)s','%(16_base3)s','%(256_yellow)s','%(256_base3)s'
+ focus = 'standout','default','%(16_base3)s','%(16_yellow)s','%(256_base3)s','%(256_yellow)s'
+ [[[authors]]]
+ normal = 'default,underline','default','%(16_cyan)s','%(16_base3)s','%(256_cyan)s','%(256_base3)s'
+ focus = 'standout','default','%(16_base2)s','%(16_yellow)s','%(256_base2)s','%(256_yellow)s'
+ width = 'fit',0,30
+ [[[subject]]]
+ normal = 'default','default','%(16_base00)s','%(16_base3)s','%(256_base00)s','%(256_base3)s'
+ focus = 'standout','default','%(16_base3)s','%(16_yellow)s','%(256_base3)s','%(256_yellow)s'
+ width = 'weight',1
+ [[[content]]]
+ normal = 'default','default','%(16_base1)s','%(16_base3)s','%(256_base1)s','%(256_base3)s'
+ focus = 'standout','default','%(16_base2)s','%(16_yellow)s','%(256_base2)s','%(256_yellow)s'
+ [[threadline-unread]]
+ normal = 'default','default','%(16_base01)s,bold','%(16_base3)s','%(256_base01)s,bold','%(256_base3)s'
+ tagged_with = 'unread'
+ [[[date]]]
+ normal = 'default','default','%(16_base01)s,bold','%(16_base3)s','%(256_base01)s,bold','%(256_base3)s'
+ [[[mailcount]]]
+ normal = 'default','default','%(16_base01)s,bold','%(16_base3)s','%(256_base01)s,bold','%(256_base3)s'
+ [[[tags]]]
+ normal = 'bold','default','%(16_yellow)s,bold','%(16_base3)s','%(256_yellow)s,bold','%(256_base3)s'
+ [[[authors]]]
+ normal = 'default,underline','default','%(16_cyan)s','%(16_base3)s','%(256_cyan)s,bold','%(256_base3)s'
+ [[[subject]]]
+ normal = 'default','default','%(16_base00)s,bold','%(16_base3)s','%(256_base00)s,bold','%(256_base3)s'
+ [[[content]]]
+ normal = 'default','default','%(16_base1)s,bold','%(16_base3)s','%(256_base1)s,bold','%(256_base3)s'
diff --git a/extra/themes/solarized_dark b/extra/themes/solarized_dark
new file mode 100644
index 00000000..ce414836
--- /dev/null
+++ b/extra/themes/solarized_dark
@@ -0,0 +1,127 @@
+###############################################################################
+# SOLARIZED DARK
+#
+# colour theme for alot. © 2012 Patrick Totzke, GNU GPL3+
+# http://ethanschoonover.com/solarized
+# https://github.com/pazz/alot
+###############################################################################
+#
+# Define mappings from solarized colour names to urwid attribute names for 16
+# and 256 colour modes. These work well assuming you use the solarized term
+# colours via Xressources/Xdefaults. You might want to change this otherwise
+
+16_base03 = 'dark gray'
+16_base02 = 'black'
+16_base01 = 'light green'
+16_base00 = 'yellow'
+16_base0 = 'light blue'
+16_base1 = 'light cyan'
+16_base2 = 'light gray'
+16_base3 = 'white'
+16_yellow = 'brown'
+16_orange = 'light red'
+16_red = 'dark red'
+16_magenta = 'dark magenta'
+16_violet = 'light magenta'
+16_blue = 'dark blue'
+16_cyan = 'dark cyan'
+16_green = 'dark green'
+
+# Use a slightly different mapping here to be able to use "bold" in 256c mode
+256_base03 = 'dark gray'
+256_base02 = 'black'
+256_base01 = 'light green'
+256_base00 = 'yellow'
+256_base0 = 'g66'
+256_base1 = 'g70'
+256_base2 = 'light gray'
+256_base3 = 'white'
+256_yellow = 'brown'
+256_orange = 'light red'
+256_red = 'dark red'
+256_magenta = 'dark magenta'
+256_violet = 'light magenta'
+256_blue = 'dark blue'
+256_cyan = '#088'
+256_green = 'dark green'
+
+
+# This is the actual alot theme
+[global]
+ footer = 'standout','default','%(16_base1)s','%(16_base02)s','%(256_base1)s','%(256_base02)s'
+ body = 'default','default','%(16_base0)s','%(16_base03)s','%(256_base0)s','%(256_base03)s'
+ notify_error = 'standout','default','%(16_base3)s','%(16_red)s','%(256_base3)s','%(256_red)s'
+ notify_normal = 'default','default','%(16_base01)s','%(16_base02)s','%(256_base01)s','%(256_base02)s'
+ prompt = 'default','default','%(16_base0)s','%(16_base02)s','%(256_base0)s','%(256_base02)s'
+ tag = 'default','default','%(16_yellow)s','%(16_base03)s','%(256_yellow)s','%(256_base03)s'
+ tag_focus = 'standout','default','%(16_base03)s','%(16_yellow)s','%(256_base03)s','%(256_yellow)s'
+[help]
+ text = 'default','default','light gray','dark gray','%(256_base0)s','%(256_base03)s'
+ section = 'underline','default','%(16_base1)s','%(16_base02)s','%(256_base1)s','%(256_base02)s'
+ title = 'standout','default','%(16_base1)s','%(16_base03)s','%(256_base1)s','%(256_base03)s'
+[taglist]
+ line_focus = 'standout','default','%(16_base02)s','%(16_yellow)s','%(256_base02)s','%(256_yellow)s'
+ line_even = 'default','default','%(16_base0)s','%(16_base03)s','%(256_base0)s','%(256_base03)s'
+ line_odd = 'default','default','%(16_base0)s','%(16_base02)s','%(256_base0)s','%(256_base02)s'
+[bufferlist]
+ line_focus = 'standout','default','%(16_base02)s','%(16_yellow)s','%(256_base02)s','%(256_yellow)s'
+ line_even = 'default','default','%(16_base0)s','%(16_base03)s','%(256_base0)s','%(256_base03)s'
+ line_odd = 'default','default','%(16_base0)s','%(16_base02)s','%(256_base0)s','%(256_base02)s'
+[thread]
+ attachment = 'default','default','%(16_base0)s','%(16_base03)s','%(256_base0)s','%(256_base03)s'
+ attachment_focus = 'underline','default','%(16_base02)s','%(16_yellow)s','%(256_base02)s','%(256_yellow)s'
+ arrow_bars = 'default','default','%(16_yellow)s','%(16_base03)s','%(256_yellow)s','%(256_base03)s'
+ arrow_heads = 'default','default','%(16_yellow)s','%(16_base03)s','%(256_yellow)s','%(256_base03)s'
+ body = 'default','default','%(16_base0)s','%(16_base03)s','%(256_base0)s','%(256_base03)s'
+ header = 'default','default','%(16_base0)s','%(16_base02)s','%(256_base0)s','%(256_base02)s'
+ header_key = 'default','default','%(16_magenta)s','%(16_base02)s','%(256_magenta)s','%(256_base02)s'
+ header_value = 'default','default','%(16_blue)s','%(16_base02)s','%(256_blue)s','%(256_base02)s'
+ [[summary]]
+ even = 'default','default','%(16_base0)s','%(16_base02)s','%(256_base0)s','%(256_base02)s'
+ focus = 'standout','default','%(16_base03)s','%(16_yellow)s','%(256_base03)s','%(256_yellow)s'
+ odd = 'default','default','%(16_base0)s','%(16_base03)s','%(256_base0)s','%(256_base03)s'
+[envelope]
+ body = 'default','default','%(16_base0)s','%(16_base03)s','%(256_base0)s','%(256_base03)s'
+ header = 'default','default','%(16_base0)s','%(16_base02)s','%(256_base0)s','%(256_base02)s'
+ header_key = 'default','default','%(16_orange)s','%(16_base02)s','%(256_orange)s','%(256_base02)s'
+ header_value = 'default','default','%(16_violet)s','%(16_base02)s','%(256_violet)s','%(256_base02)s'
+[search]
+ [[threadline]]
+ normal = 'default','default','%(16_base1)s','%(16_base03)s','%(256_base1)s','%(256_base03)s'
+ focus = 'standout','default','%(16_base02)s','%(16_yellow)s','%(256_base02)s','%(256_yellow)s'
+ parts = date,mailcount,tags,authors,subject
+ [[[date]]]
+ normal = 'default','default','%(16_base1)s','%(16_base03)s','%(256_base1)s','%(256_base03)s'
+ focus = 'standout','default','%(16_base02)s,bold','%(16_yellow)s','%(256_base02)s,bold','%(256_yellow)s'
+ [[[mailcount]]]
+ normal = 'default','default','%(16_base1)s','%(16_base03)s','%(256_base1)s','%(256_base03)s'
+ focus = 'standout','default','%(16_base02)s','%(16_yellow)s','%(256_base02)s','%(256_yellow)s'
+ [[[tags]]]
+ normal = 'bold','default','%(16_yellow)s','%(16_base03)s','%(256_yellow)s','%(256_base03)s'
+ focus = 'standout','default','%(16_base02)s,bold','%(16_yellow)s','%(256_base02)s,bold','%(256_yellow)s'
+ [[[authors]]]
+ normal = 'default,underline','default','%(16_cyan)s','%(16_base03)s','%(256_cyan)s','%(256_base03)s'
+ focus = 'standout','default','%(16_base02)s','%(16_yellow)s','%(256_base02)s','%(256_yellow)s'
+ width = 'fit',0,30
+ [[[subject]]]
+ normal = 'default','default','%(16_base0)s','%(16_base03)s','%(256_base0)s','%(256_base03)s'
+ focus = 'standout','default','%(16_base02)s,bold','%(16_yellow)s','%(256_base02)s,bold','%(256_yellow)s'
+ width = 'weight',1
+ [[[content]]]
+ normal = 'default','default','%(16_base01)s','%(16_base03)s','%(256_base01)s','%(256_base03)s'
+ focus = 'standout','default','%(16_base02)s','%(16_yellow)s','%(256_base02)s','%(256_yellow)s'
+ [[threadline-unread]]
+ normal = 'default','default','%(16_base1)s,bold','%(16_base03)s','%(256_base1)s,bold','%(256_base03)s'
+ tagged_with = 'unread'
+ [[[date]]]
+ normal = 'default','default','%(16_base1)s,bold','%(16_base03)s','%(256_base1)s,bold','%(256_base03)s'
+ [[[mailcount]]]
+ normal = 'default','default','%(16_base1)s,bold','%(16_base03)s','%(256_base1)s,bold','%(256_base03)s'
+ [[[tags]]]
+ normal = 'bold','default','%(16_yellow)s,bold','%(16_base03)s','%(256_yellow)s,bold','%(256_base03)s'
+ [[[authors]]]
+ normal = 'default,underline','default','%(16_cyan)s','%(16_base03)s','%(256_cyan)s,bold','%(256_base03)s'
+ [[[subject]]]
+ normal = 'default','default','%(16_base0)s,bold','%(16_base03)s','%(256_base0)s,bold','%(256_base03)s'
+ [[[content]]]
+ normal = 'default','default','%(16_base01)s,bold','%(16_base03)s','%(256_base01)s,bold','%(256_base03)s'