summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Totzke <patricktotzke@gmail.com>2012-08-10 13:04:13 +0100
committerPatrick Totzke <patricktotzke@gmail.com>2012-08-10 13:04:13 +0100
commit74bf8409765c1c3a415c4850c397fc8d3936b957 (patch)
tree68aac11809c95af16d2598618769dc95abf28a21
parent6c80ee5ecd23ec54604eab3ada0e2573e1757d89 (diff)
parent2e414327e80fe59a55b9a5676bf38bebec604fed (diff)
Merge branch 'cleanup'
-rw-r--r--alot/account.py2
-rw-r--r--alot/buffers.py24
-rw-r--r--alot/commands/__init__.py4
-rw-r--r--alot/commands/envelope.py20
-rw-r--r--alot/commands/globals.py39
-rw-r--r--alot/commands/search.py6
-rw-r--r--alot/commands/thread.py31
-rw-r--r--alot/db/__init__.py361
-rw-r--r--alot/db/manager.py363
-rwxr-xr-xalot/init.py10
-rw-r--r--alot/settings/__init__.py394
-rw-r--r--alot/settings/manager.py394
-rw-r--r--alot/ui.py20
-rw-r--r--alot/widgets.py738
-rw-r--r--alot/widgets/__init__.py0
-rw-r--r--alot/widgets/bufferlist.py29
-rw-r--r--alot/widgets/globals.py203
-rw-r--r--alot/widgets/search.py186
-rw-r--r--alot/widgets/thread.py301
-rw-r--r--alot/widgets/utils.py64
-rw-r--r--docs/source/api/database.rst18
-rw-r--r--docs/source/api/interface.rst24
-rw-r--r--docs/source/api/settings.rst39
-rw-r--r--docs/source/conf.py11
-rwxr-xr-xsetup.py2
25 files changed, 1687 insertions, 1596 deletions
diff --git a/alot/account.py b/alot/account.py
index 6bee5706..1686bff7 100644
--- a/alot/account.py
+++ b/alot/account.py
@@ -3,13 +3,11 @@
# For further details see the COPYING file
import mailbox
import logging
-import time
import os
import glob
from alot.helper import call_cmd_async
from alot.helper import split_commandstring
-import alot.crypto as crypto
class SendingMailFailed(RuntimeError):
diff --git a/alot/buffers.py b/alot/buffers.py
index 65b33798..79851715 100644
--- a/alot/buffers.py
+++ b/alot/buffers.py
@@ -5,13 +5,19 @@ import urwid
import os
from notmuch import NotmuchError
-import widgets
from settings import settings
import commands
from walker import PipeWalker
from helper import shorten_author_string
from db.errors import NonexistantObjectError
+from alot.widgets.globals import TagWidget
+from alot.widgets.globals import HeadersList
+from alot.widgets.globals import AttachmentWidget
+from alot.widgets.bufferlist import BufferlineWidget
+from alot.widgets.search import ThreadlineWidget
+from alot.widgets.thread import MessageWidget
+
class Buffer(object):
"""Abstract base class for buffers."""
@@ -76,7 +82,7 @@ class BufferlistBuffer(Buffer):
lines = list()
displayedbuffers = filter(self.filtfun, self.ui.buffers)
for (num, b) in enumerate(displayedbuffers):
- line = widgets.BufferlineWidget(b)
+ line = BufferlineWidget(b)
if (num % 2) == 0:
attr = settings.get_theming_attribute('bufferlist',
'line_even')
@@ -148,13 +154,13 @@ class EnvelopeBuffer(Buffer):
key_att = settings.get_theming_attribute('envelope', 'header_key')
value_att = settings.get_theming_attribute('envelope',
'header_value')
- self.header_wgt = widgets.HeadersList(lines, key_att, value_att)
+ self.header_wgt = HeadersList(lines, key_att, value_att)
displayed_widgets.append(self.header_wgt)
#display attachments
lines = []
for a in self.envelope.attachments:
- lines.append(widgets.AttachmentWidget(a, selectable=False))
+ lines.append(AttachmentWidget(a, selectable=False))
if lines:
self.attachment_wgt = urwid.Pile(lines)
displayed_widgets.append(self.attachment_wgt)
@@ -232,7 +238,7 @@ class SearchBuffer(Buffer):
self.body = self.listbox
return
- self.threadlist = PipeWalker(self.pipe, widgets.ThreadlineWidget,
+ self.threadlist = PipeWalker(self.pipe, ThreadlineWidget,
dbman=self.dbman)
self.listbox = urwid.ListBox(self.threadlist)
@@ -318,9 +324,9 @@ class ThreadBuffer(Buffer):
childcount[p] -= 1
bars.append(childcount[p] > 0)
- mwidget = widgets.MessageWidget(m, even=(num % 2 == 0),
- depth=depth,
- bars_at=bars)
+ mwidget = MessageWidget(m, even=(num % 2 == 0),
+ depth=depth,
+ bars_at=bars)
msglines.append(mwidget)
self.body = urwid.ListBox(msglines)
@@ -396,7 +402,7 @@ class TagListBuffer(Buffer):
attr = settings.get_theming_attribute('taglist', 'line_odd')
focus_att = settings.get_theming_attribute('taglist', 'line_focus')
- tw = widgets.TagWidget(b, attr, focus_att)
+ tw = TagWidget(b, attr, focus_att)
rows = [('fixed', tw.width(), tw)]
if tw.hidden:
rows.append(urwid.Text(b + ' [hidden]'))
diff --git a/alot/commands/__init__.py b/alot/commands/__init__.py
index 53cc4cfd..d3a052d0 100644
--- a/alot/commands/__init__.py
+++ b/alot/commands/__init__.py
@@ -196,9 +196,9 @@ def commandfactory(cmdline, mode='global'):
# set pre and post command hooks
get_hook = settings.get_hook
parms['prehook'] = get_hook('pre_%s_%s' % (mode, cmdname)) or \
- get_hook('pre_global_%s' % cmdname)
+ get_hook('pre_global_%s' % cmdname)
parms['posthook'] = get_hook('post_%s_%s' % (mode, cmdname)) or \
- get_hook('post_global_%s' % cmdname)
+ get_hook('post_global_%s' % cmdname)
logging.debug('cmd parms %s' % parms)
return cmdclass(**parms)
diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py
index 89d3b5d8..b4c5604d 100644
--- a/alot/commands/envelope.py
+++ b/alot/commands/envelope.py
@@ -84,14 +84,14 @@ class SaveCommand(Command):
# determine account to use
sname, saddr = email.Utils.parseaddr(envelope.get('From'))
account = settings.get_account_by_address(saddr)
- if account == None:
+ if account is None:
if not settings.get_accounts():
ui.notify('no accounts set.', priority='error')
return
else:
account = settings.get_accounts()[0]
- if account.draft_box == None:
+ if account.draft_box is None:
ui.notify('abort: account <%s> has no draft_box set.' % saddr,
priority='error')
return
@@ -130,7 +130,7 @@ class SendCommand(Command):
# determine account to use for sending
account = settings.get_account_by_address(saddr)
- if account == None:
+ if account is None:
if not settings.get_accounts():
ui.notify('no accounts set', priority='error')
return
@@ -185,8 +185,8 @@ class SendCommand(Command):
(['--spawn'], {'action': BooleanAction, 'default':None,
'help':'spawn editor in new terminal'}),
(['--refocus'], {'action': BooleanAction, 'default':True,
- 'help':'refocus envelope after editing'}),
- ])
+ 'help':'refocus envelope after editing'}),
+])
class EditCommand(Command):
"""edit mail"""
def __init__(self, envelope=None, spawn=None, refocus=True, **kwargs):
@@ -198,7 +198,7 @@ class EditCommand(Command):
:param refocus: m
"""
self.envelope = envelope
- self.openNew = (envelope != None)
+ self.openNew = (envelope is not None)
self.force_spawn = spawn
self.refocus = refocus
self.edit_only_body = False
@@ -280,15 +280,15 @@ class EditCommand(Command):
if self.envelope.tmpfile:
old_tmpfile = self.envelope.tmpfile
self.envelope.tmpfile = tempfile.NamedTemporaryFile(delete=False,
- prefix='alot.')
+ prefix='alot.')
self.envelope.tmpfile.write(content.encode('utf-8'))
self.envelope.tmpfile.flush()
self.envelope.tmpfile.close()
if old_tmpfile:
os.unlink(old_tmpfile.name)
cmd = globals.EditCommand(self.envelope.tmpfile.name,
- on_success=openEnvelopeFromTmpfile, spawn=self.force_spawn,
- thread=self.force_spawn, refocus=self.refocus)
+ on_success=openEnvelopeFromTmpfile, spawn=self.force_spawn,
+ thread=self.force_spawn, refocus=self.refocus)
ui.apply_command(cmd)
@@ -347,7 +347,7 @@ class ToggleHeaderCommand(Command):
(['keyid'], {'nargs':argparse.REMAINDER, 'help':'which key id to use'})],
help='mark mail to be signed before sending')
@registerCommand(MODE, 'unsign', forced={'action': 'unsign'},
- help='mark mail not to be signed before sending')
+ help='mark mail not to be signed before sending')
@registerCommand(MODE, 'togglesign', forced={'action': 'toggle'}, arguments=[
(['keyid'], {'nargs':argparse.REMAINDER, 'help':'which key id to use'})],
help='toggle sign status')
diff --git a/alot/commands/globals.py b/alot/commands/globals.py
index d9bce6ff..13d876e4 100644
--- a/alot/commands/globals.py
+++ b/alot/commands/globals.py
@@ -18,16 +18,14 @@ from alot.completion import CommandLineCompleter
from alot.commands import CommandParseError
from alot.commands import commandfactory
from alot import buffers
-from alot import widgets
+from alot.widgets.utils import DialogBox
from alot import helper
-from alot import crypto
from alot.db.errors import DatabaseLockedError
from alot.completion import ContactsCompleter
from alot.completion import AccountCompleter
from alot.db.envelope import Envelope
from alot import commands
from alot.settings import settings
-from alot.errors import GPGProblem
from alot.helper import split_commandstring
from alot.utils.booleanaction import BooleanAction
@@ -41,7 +39,7 @@ class ExitCommand(Command):
def apply(self, ui):
if settings.get('bug_on_exit'):
if (yield ui.choice('realy quit?', select='yes', cancel='no',
- msg_position='left')) == 'no':
+ msg_position='left')) == 'no':
return
for b in ui.buffers:
b.cleanup()
@@ -50,7 +48,7 @@ class ExitCommand(Command):
@registerCommand(MODE, 'search', usage='search query', arguments=[
(['--sort'], {'help':'sort order', 'choices':[
- 'oldest_first', 'newest_first', 'message_id', 'unsorted']}),
+ 'oldest_first', 'newest_first', 'message_id', 'unsorted']}),
(['query'], {'nargs':argparse.REMAINDER, 'help':'search string'})])
class SearchCommand(Command):
"""open a new search buffer"""
@@ -218,7 +216,7 @@ class ExternalCommand(Command):
def thread_code(*args):
try:
- if stdin == None:
+ if stdin is None:
proc = subprocess.Popen(self.cmdlist, shell=self.shell,
stderr=subprocess.PIPE)
ret = proc.wait()
@@ -290,7 +288,7 @@ class EditCommand(ExternalCommand):
**kwargs)
def apply(self, ui):
- if self.cmdlist == None:
+ if self.cmdlist is None:
ui.notify('no editor set', priority='error')
else:
return ExternalCommand.apply(self, ui)
@@ -318,7 +316,6 @@ class CallCommand(Command):
self.command = command
def apply(self, ui):
- hooks = settings.hooks
try:
exec self.command
except Exception as e:
@@ -346,7 +343,7 @@ class BufferCloseCommand(Command):
@inlineCallbacks
def apply(self, ui):
- if self.buffer == None:
+ if self.buffer is None:
self.buffer = ui.current_buffer
if (isinstance(self.buffer, buffers.EnvelopeBuffer) and
@@ -362,7 +359,7 @@ class BufferCloseCommand(Command):
ui.apply_command(ExitCommand())
else:
logging.info('not closing last remaining buffer as '
- 'global.quit_on_last_bclose is set to False')
+ 'global.quit_on_last_bclose is set to False')
else:
ui.buffer_close(self.buffer)
@@ -494,7 +491,7 @@ class HelpCommand(Command):
# mode specific maps
if modemaps:
linewidgets.append(urwid.Text((section_att,
- '\n%s-mode specific maps' % ui.mode)))
+ '\n%s-mode specific maps' % ui.mode)))
for (k, v) in modemaps.items():
line = urwid.Columns([('fixed', keycolumnwidth,
urwid.Text((text_att, k))),
@@ -514,9 +511,9 @@ class HelpCommand(Command):
ckey = 'cancel'
titletext = 'Bindings Help (%s cancels)' % ckey
- box = widgets.DialogBox(body, titletext,
- bodyattr=text_att,
- titleattr=title_att)
+ box = DialogBox(body, titletext,
+ bodyattr=text_att,
+ titleattr=title_att)
# put promptwidget as overlay on main widget
overlay = urwid.Overlay(box, ui.mainframe, 'center',
@@ -595,7 +592,7 @@ class ComposeCommand(Command):
@inlineCallbacks
def apply(self, ui):
- if self.envelope == None:
+ if self.envelope is None:
self.envelope = Envelope()
if self.template is not None:
#get location of tempsdir, containing msg templates
@@ -687,7 +684,7 @@ class ComposeCommand(Command):
ui.notify('could not locate signature: %s' % sig,
priority='error')
if (yield ui.choice('send without signature',
- select='yes', cancel='no')) == 'no':
+ select='yes', cancel='no')) == 'no':
return
# Figure out whether we should GPG sign messages by default
@@ -705,23 +702,23 @@ class ComposeCommand(Command):
logging.debug(allbooks)
if account is not None:
abooks = settings.get_addressbooks(order=[account],
- append_remaining=allbooks)
+ append_remaining=allbooks)
logging.debug(abooks)
completer = ContactsCompleter(abooks)
else:
completer = None
to = yield ui.prompt('To',
completer=completer)
- if to == None:
+ if to is None:
ui.notify('canceled')
return
self.envelope.add('To', to.strip(' \t\n,'))
if settings.get('ask_subject') and \
- not 'Subject' in self.envelope.headers:
+ not 'Subject' in self.envelope.headers:
subject = yield ui.prompt('Subject')
logging.debug('SUBJECT: "%s"' % subject)
- if subject == None:
+ if subject is None:
ui.notify('canceled')
return
self.envelope.add('Subject', subject)
@@ -733,7 +730,7 @@ class ComposeCommand(Command):
logging.debug('attaching: ' + a)
cmd = commands.envelope.EditCommand(envelope=self.envelope,
- spawn=self.force_spawn, refocus=False)
+ spawn=self.force_spawn, refocus=False)
ui.apply_command(cmd)
diff --git a/alot/commands/search.py b/alot/commands/search.py
index 22bca06b..51a3fdd6 100644
--- a/alot/commands/search.py
+++ b/alot/commands/search.py
@@ -39,11 +39,11 @@ class OpenThreadCommand(Command):
@registerCommand(MODE, 'refine', help='refine query', arguments=[
(['--sort'], {'help':'sort order', 'choices':[
- 'oldest_first', 'newest_first', 'message_id', 'unsorted']}),
+ 'oldest_first', 'newest_first', 'message_id', 'unsorted']}),
(['query'], {'nargs':argparse.REMAINDER, 'help':'search string'})])
@registerCommand(MODE, 'sort', help='set sort order', arguments=[
(['sort'], {'help':'sort order', 'choices':[
- 'oldest_first', 'newest_first', 'message_id', 'unsorted']}),
+ 'oldest_first', 'newest_first', 'message_id', 'unsorted']}),
])
class RefineCommand(Command):
"""refine the querystring of this buffer"""
@@ -186,7 +186,7 @@ class TagCommand(Command):
thread.add_tags(tags, afterwards=refresh)
if self.action == 'set':
thread.add_tags(tags, afterwards=refresh,
- remove_rest=True)
+ remove_rest=True)
elif self.action == 'remove':
thread.remove_tags(tags, afterwards=refresh)
elif self.action == 'toggle':
diff --git a/alot/commands/thread.py b/alot/commands/thread.py
index f6b79595..079e2a10 100644
--- a/alot/commands/thread.py
+++ b/alot/commands/thread.py
@@ -5,7 +5,6 @@ import os
import logging
import tempfile
from twisted.internet.defer import inlineCallbacks
-import re
import subprocess
from email.Utils import parseaddr
import mailcap
@@ -15,8 +14,6 @@ from alot.commands import Command, registerCommand
from alot.commands.globals import ExternalCommand
from alot.commands.globals import FlushCommand
from alot.commands.globals import ComposeCommand
-from alot.commands.globals import RefreshCommand
-from alot import widgets
from alot import completion
from alot.db.utils import decode_header
from alot.db.utils import encode_header
@@ -30,6 +27,9 @@ from alot.helper import parse_mailcap_nametemplate
from alot.helper import split_commandstring
from alot.utils.booleanaction import BooleanAction
+from alot.widgets.globals import AttachmentWidget
+from alot.widgets.thread import MessageSummaryWidget
+
MODE = 'thread'
@@ -226,7 +226,8 @@ class ForwardCommand(Command):
if qf:
quote = qf(name, address, timestamp, ui=ui, dbm=ui.dbman)
else:
- quote = 'Forwarded message from %s (%s):\n' % (name or address, timestamp)
+ quote = 'Forwarded message from %s (%s):\n' % (
+ name or address, timestamp)
mailcontent = quote
quotehook = settings.get_hook('text_quote')
if quotehook:
@@ -317,9 +318,9 @@ class EditNewCommand(Command):
(['--all'], {'action': 'store_true', 'help':'affect all messages'})],
help='display message source')
@registerCommand(MODE, 'toggleheaders', forced={'all_headers': 'toggle'},
- arguments=[
- (['--all'], {'action': 'store_true', 'help':'affect all messages'})],
- help='display all headers')
+ arguments=[
+ (['--all'], {'action': 'store_true', 'help':'affect all messages'})],
+ help='display all headers')
class ChangeDisplaymodeCommand(Command):
"""fold or unfold messages"""
def __init__(self, all=False, visible=None, raw=None, all_headers=None,
@@ -383,11 +384,11 @@ class ChangeDisplaymodeCommand(Command):
(['--background'], {'action': 'store_true',
'help':'don\'t stop the interface'}),
(['--add_tags'], {'action': 'store_true',
- 'help':'add \'Tags\' header to the message'}),
+ 'help':'add \'Tags\' header to the message'}),
(['--shell'], {'action': 'store_true',
- 'help':'let the shell interpret the command'}),
+ 'help':'let the shell interpret the command'}),
(['--notify_stdout'], {'action': 'store_true',
- 'help':'display command\'s stdout as notification message'}),
+ 'help':'display command\'s stdout as notification message'}),
],
)
class PipeCommand(Command):
@@ -574,7 +575,7 @@ class RemoveCommand(Command):
(['--separately'], {'action': 'store_true',
'help':'call print command once for each message'}),
(['--add_tags'], {'action': 'store_true',
- 'help':'add \'Tags\' header to the message'}),
+ 'help':'add \'Tags\' header to the message'}),
],
)
class PrintCommand(PipeCommand):
@@ -604,7 +605,7 @@ class PrintCommand(PipeCommand):
# no print cmd set
noop_msg = 'no print command specified. Set "print_cmd" in the '\
- 'global section.'
+ 'global section.'
PipeCommand.__init__(self, [cmd], all=all, separately=separately,
background=True,
@@ -658,7 +659,7 @@ class SaveAttachmentCommand(Command):
ui.notify('canceled')
else: # save focussed attachment
focus = ui.get_deep_focus()
- if isinstance(focus, widgets.AttachmentWidget):
+ if isinstance(focus, AttachmentWidget):
attachment = focus.get_attachment()
filename = attachment.get_filename()
if not self.path:
@@ -747,9 +748,9 @@ class ThreadSelectCommand(Command):
- if attachment line, this opens the attachment"""
def apply(self, ui):
focus = ui.get_deep_focus()
- if isinstance(focus, widgets.MessageSummaryWidget):
+ if isinstance(focus, MessageSummaryWidget):
ui.apply_command(ChangeDisplaymodeCommand(visible='toggle'))
- elif isinstance(focus, widgets.AttachmentWidget):
+ elif isinstance(focus, AttachmentWidget):
logging.info('open attachment')
ui.apply_command(OpenAttachmentCommand(focus.get_attachment()))
else:
diff --git a/alot/db/__init__.py b/alot/db/__init__.py
index beb630d6..f61142ea 100644
--- a/alot/db/__init__.py
+++ b/alot/db/__init__.py
@@ -1,364 +1,7 @@
# 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
-from notmuch import Database, NotmuchError, XapianError
-import notmuch
-import multiprocessing
-import logging
-from collections import deque
-
-from message import Message
-from alot.settings import settings
from thread import Thread
-from errors import DatabaseError
-from errors import DatabaseLockedError
-from errors import DatabaseROError
-from errors import NonexistantObjectError
-
-DB_ENC = 'utf-8'
-
-
-class FillPipeProcess(multiprocessing.Process):
- def __init__(self, it, pipe, fun=(lambda x: x)):
- multiprocessing.Process.__init__(self)
- self.it = it
- self.pipe = pipe[1]
- self.fun = fun
-
- def run(self):
- for a in self.it:
- self.pipe.send(self.fun(a))
- self.pipe.close()
-
-
-class DBManager(object):
- """
- Keeps track of your index parameters, maintains a write-queue and
- lets you look up threads and messages directly to the persistent wrapper
- classes.
- """
- _sort_orders = {
- 'oldest_first': notmuch.database.Query.SORT.OLDEST_FIRST,
- 'newest_first': notmuch.database.Query.SORT.NEWEST_FIRST,
- 'unsorted': notmuch.database.Query.SORT.UNSORTED,
- 'message_id': notmuch.database.Query.SORT.MESSAGE_ID,
- }
- """constants representing sort orders"""
-
- def __init__(self, path=None, ro=False):
- """
- :param path: absolute path to the notmuch index
- :type path: str
- :param ro: open the index in read-only mode
- :type ro: bool
- """
- self.ro = ro
- self.path = path
- self.writequeue = deque([])
- self.processes = []
-
- def flush(self):
- """
- write out all queued write-commands in order, each one in a separate
- :meth:`atomic <notmuch.Database.begin_atomic>` transaction.
-
- If this fails the current action is rolled back, stays in the write
- queue and an exception is raised.
- You are responsible to retry flushing at a later time if you want to
- ensure that the cached changes are applied to the database.
-
- :exception: :exc:`~errors.DatabaseROError` if db is opened read-only
- :exception: :exc:`~errors.DatabaseLockedError` if db is locked
- """
- if self.ro:
- raise DatabaseROError()
- if self.writequeue:
- # read notmuch's config regarding imap flag synchronization
- sync = settings.get_notmuch_setting('maildir', 'synchronize_flags')
-
- # go through writequeue entries
- while self.writequeue:
- current_item = self.writequeue.popleft()
- logging.debug('write-out item: %s' % str(current_item))
-
- # watch out for notmuch errors to re-insert current_item
- # to the queue on errors
- try:
- # the first two coordinants are cnmdname and post-callback
- cmd, afterwards = current_item[:2]
- logging.debug('cmd created')
-
- # aquire a writeable db handler
- try:
- mode = Database.MODE.READ_WRITE
- db = Database(path=self.path, mode=mode)
- except NotmuchError:
- raise DatabaseLockedError()
- logging.debug('got write lock')
-
- # make this a transaction
- db.begin_atomic()
- logging.debug('got atomic')
-
- if cmd == 'add':
- logging.debug('add')
- path, tags = current_item[2:]
- msg, status = db.add_message(path,
- sync_maildir_flags=sync)
- logging.debug('added msg')
- msg.freeze()
- logging.debug('freeze')
- for tag in tags:
- msg.add_tag(tag.encode(DB_ENC),
- sync_maildir_flags=sync)
- logging.debug('added tags ')
- msg.thaw()
- logging.debug('thaw')
-
- elif cmd == 'remove':
- path = current_item[2]
- db.remove_message(path)
-
- else: # tag/set/untag
- querystring, tags = current_item[2:]
- query = db.create_query(querystring)
- for msg in query.search_messages():
- msg.freeze()
- if cmd == 'tag':
- for tag in tags:
- msg.add_tag(tag.encode(DB_ENC),
- sync_maildir_flags=sync)
- if cmd == 'set':
- msg.remove_all_tags()
- for tag in tags:
- msg.add_tag(tag.encode(DB_ENC),
- sync_maildir_flags=sync)
- elif cmd == 'untag':
- for tag in tags:
- msg.remove_tag(tag.encode(DB_ENC),
- sync_maildir_flags=sync)
- msg.thaw()
-
- logging.debug('ended atomic')
- # end transaction and reinsert queue item on error
- if db.end_atomic() != notmuch.STATUS.SUCCESS:
- raise DatabaseError('end_atomic failed')
- logging.debug('ended atomic')
-
- # close db
- db.close()
- logging.debug('closed db')
-
- # call post-callback
- if callable(afterwards):
- logging.debug(str(afterwards))
- afterwards()
- logging.debug('called callback')
-
- # re-insert item to the queue upon Xapian/NotmuchErrors
- except (XapianError, NotmuchError) as e:
- logging.exception(e)
- self.writequeue.appendleft(current_item)
- raise DatabaseError(unicode(e))
- except DatabaseLockedError as e:
- logging.debug('index temporarily locked')
- self.writequeue.appendleft(current_item)
- raise e
- logging.debug('flush finished')
-
- def kill_search_processes(self):
- """
- terminate all search processes that originate from
- this managers :meth:`get_threads`.
- """
- for p in self.processes:
- p.terminate()
- self.processes = []
-
- def tag(self, querystring, tags, afterwards=None, remove_rest=False):
- """
- add tags to messages matching `querystring`.
- This appends a tag operation to the write queue and raises
- :exc:`~errors.DatabaseROError` if in read only mode.
-
- :param querystring: notmuch search string
- :type querystring: str
- :param tags: a list of tags to be added
- :type tags: list of str
- :param afterwards: callback that gets called after successful
- application of this tagging operation
- :type afterwards: callable
- :param remove_rest: remove tags from matching messages before tagging
- :type remove_rest: bool
- :exception: :exc:`~errors.DatabaseROError`
-
- .. note::
- This only adds the requested operation to the write queue.
- You need to call :meth:`DBManager.flush` to actually write out.
- """
- if self.ro:
- raise DatabaseROError()
- if remove_rest:
- self.writequeue.append(('set', afterwards, querystring, tags))
- else:
- self.writequeue.append(('tag', afterwards, querystring, tags))
-
- def untag(self, querystring, tags, afterwards=None):
- """
- removes tags from messages that match `querystring`.
- This appends an untag operation to the write queue and raises
- :exc:`~errors.DatabaseROError` if in read only mode.
-
- :param querystring: notmuch search string
- :type querystring: str
- :param tags: a list of tags to be added
- :type tags: list of str
- :param afterwards: callback that gets called after successful
- application of this tagging operation
- :type afterwards: callable
- :exception: :exc:`~errors.DatabaseROError`
-
- .. note::
- This only adds the requested operation to the write queue.
- You need to call :meth:`DBManager.flush` to actually write out.
- """
- if self.ro:
- raise DatabaseROError()
- self.writequeue.append(('untag', afterwards, querystring, tags))
-
- def count_messages(self, querystring):
- """returns number of messages that match `querystring`"""
- return self.query(querystring).count_messages()
-
- def count_threads(self, querystring):
- """returns number of threads that match `querystring`"""
- return self.query(querystring).count_threads()
-
- def search_thread_ids(self, querystring):
- """
- returns the ids of all threads that match the `querystring`
- This copies! all integer thread ids into an new list.
-
- :returns: list of str
- """
-
- return self.query_threaded(querystring)
-
- def _get_notmuch_thread(self, tid):
- """returns :class:`notmuch.database.Thread` with given id"""
- query = self.query('thread:' + tid)
- try:
- return query.search_threads().next()
- except StopIteration:
- errmsg = 'no thread with id %s exists!' % tid
- raise NonexistantObjectError(errmsg)
-
- def get_thread(self, tid):
- """returns :class:`Thread` with given thread id (str)"""
- return Thread(self, self._get_notmuch_thread(tid))
-
- def _get_notmuch_message(self, mid):
- """returns :class:`notmuch.database.Message` with given id"""
- mode = Database.MODE.READ_ONLY
- db = Database(path=self.path, mode=mode)
- try:
- return db.find_message(mid)
- except:
- errmsg = 'no message with id %s exists!' % mid
- raise NonexistantObjectError(errmsg)
-
- def get_message(self, mid):
- """returns :class:`Message` with given message id (str)"""
- return Message(self, self._get_notmuch_message(mid))
-
- def get_all_tags(self):
- """
- returns all tagsstrings used in the database
- :rtype: list of str
- """
- db = Database(path=self.path)
- return [t for t in db.get_all_tags()]
-
- def async(self, cbl, fun):
- """
- return a pair (pipe, process) so that the process writes
- `fun(a)` to the pipe for each element `a` in the iterable returned
- by the callable `cbl`.
-
- :param cbl: a function returning something iterable
- :type cbl: callable
- :param fun: an unary translation function
- :type fun: callable
- :rtype: (:class:`multiprocessing.Pipe`,
- :class:`multiprocessing.Process`)
- """
- pipe = multiprocessing.Pipe(False)
- receiver, sender = pipe
- process = FillPipeProcess(cbl(), pipe, fun)
- process.start()
- self.processes.append(process)
- # closing the sending end in this (receiving) process guarantees
- # that here the apropriate EOFError is raised upon .recv in the walker
- sender.close()
- return receiver, process
-
- def get_threads(self, querystring, sort='newest_first'):
- """
- asynchronously look up thread ids matching `querystring`.
-
- :param querystring: The query string to use for the lookup
- :type querystring: str.
- :param sort: Sort order. one of ['oldest_first', 'newest_first',
- 'message_id', 'unsorted']
- :type query: str
- :returns: a pipe together with the process that asynchronously
- writes to it.
- :rtype: (:class:`multiprocessing.Pipe`,
- :class:`multiprocessing.Process`)
- """
- assert sort in self._sort_orders.keys()
- q = self.query(querystring)
- q.set_sort(self._sort_orders[sort])
- return self.async(q.search_threads, (lambda a: a.get_thread_id()))
-
- def query(self, querystring):
- """
- creates :class:`notmuch.Query` objects on demand
-
- :param querystring: The query string to use for the lookup
- :type query: str.
- :returns: :class:`notmuch.Query` -- the query object.
- """
- mode = Database.MODE.READ_ONLY
- db = Database(path=self.path, mode=mode)
- return db.create_query(querystring)
-
- def add_message(self, path, tags=[], afterwards=None):
- """
- Adds a file to the notmuch index.
-
- :param path: path to the file
- :type path: str
- :param tags: tagstrings to add
- :type tags: list of str
- :param afterwards: callback to trigger after adding
- :type afterwards: callable or None
- """
- if self.ro:
- raise DatabaseROError()
- self.writequeue.append(('add', afterwards, path, tags))
-
- def remove_message(self, message, afterwards=None):
- """
- Remove a message from the notmuch index
-
- :param message: message to remove
- :type message: :class:`Message`
- :param afterwards: callback to trigger after removing
- :type afterwards: callable or None
- """
- if self.ro:
- raise DatabaseROError()
- path = message.get_filename()
- self.writequeue.append(('remove', afterwards, path))
+from message import Message
+DB_ENC = 'UTF-8'
diff --git a/alot/db/manager.py b/alot/db/manager.py
new file mode 100644
index 00000000..c52da7b2
--- /dev/null
+++ b/alot/db/manager.py
@@ -0,0 +1,363 @@
+# 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
+from notmuch import Database, NotmuchError, XapianError
+import notmuch
+import multiprocessing
+import logging
+
+from collections import deque
+
+from message import Message
+from alot.settings import settings
+from thread import Thread
+from errors import DatabaseError
+from errors import DatabaseLockedError
+from errors import DatabaseROError
+from errors import NonexistantObjectError
+from alot.db import DB_ENC
+
+
+class FillPipeProcess(multiprocessing.Process):
+ def __init__(self, it, pipe, fun=(lambda x: x)):
+ multiprocessing.Process.__init__(self)
+ self.it = it
+ self.pipe = pipe[1]
+ self.fun = fun
+
+ def run(self):
+ for a in self.it:
+ self.pipe.send(self.fun(a))
+ self.pipe.close()
+
+
+class DBManager(object):
+ """
+ Keeps track of your index parameters, maintains a write-queue and
+ lets you look up threads and messages directly to the persistent wrapper
+ classes.
+ """
+ _sort_orders = {
+ 'oldest_first': notmuch.database.Query.SORT.OLDEST_FIRST,
+ 'newest_first': notmuch.database.Query.SORT.NEWEST_FIRST,
+ 'unsorted': notmuch.database.Query.SORT.UNSORTED,
+ 'message_id': notmuch.database.Query.SORT.MESSAGE_ID,
+ }
+ """constants representing sort orders"""
+
+ def __init__(self, path=None, ro=False):
+ """
+ :param path: absolute path to the notmuch index
+ :type path: str
+ :param ro: open the index in read-only mode
+ :type ro: bool
+ """
+ self.ro = ro
+ self.path = path
+ self.writequeue = deque([])
+ self.processes = []
+
+ def flush(self):
+ """
+ write out all queued write-commands in order, each one in a separate
+ :meth:`atomic <notmuch.Database.begin_atomic>` transaction.
+
+ If this fails the current action is rolled back, stays in the write
+ queue and an exception is raised.
+ You are responsible to retry flushing at a later time if you want to
+ ensure that the cached changes are applied to the database.
+
+ :exception: :exc:`~errors.DatabaseROError` if db is opened read-only
+ :exception: :exc:`~errors.DatabaseLockedError` if db is locked
+ """
+ if self.ro:
+ raise DatabaseROError()
+ if self.writequeue:
+ # read notmuch's config regarding imap flag synchronization
+ sync = settings.get_notmuch_setting('maildir', 'synchronize_flags')
+
+ # go through writequeue entries
+ while self.writequeue:
+ current_item = self.writequeue.popleft()
+ logging.debug('write-out item: %s' % str(current_item))
+
+ # watch out for notmuch errors to re-insert current_item
+ # to the queue on errors
+ try:
+ # the first two coordinants are cnmdname and post-callback
+ cmd, afterwards = current_item[:2]
+ logging.debug('cmd created')
+
+ # aquire a writeable db handler
+ try:
+ mode = Database.MODE.READ_WRITE
+ db = Database(path=self.path, mode=mode)
+ except NotmuchError:
+ raise DatabaseLockedError()
+ logging.debug('got write lock')
+
+ # make this a transaction
+ db.begin_atomic()
+ logging.debug('got atomic')
+
+ if cmd == 'add':
+ logging.debug('add')
+ path, tags = current_item[2:]
+ msg, status = db.add_message(path,
+ sync_maildir_flags=sync)
+ logging.debug('added msg')
+ msg.freeze()
+ logging.debug('freeze')
+ for tag in tags:
+ msg.add_tag(tag.encode(DB_ENC),
+ sync_maildir_flags=sync)
+ logging.debug('added tags ')
+ msg.thaw()
+ logging.debug('thaw')
+
+ elif cmd == 'remove':
+ path = current_item[2]
+ db.remove_message(path)
+
+ else: # tag/set/untag
+ querystring, tags = current_item[2:]
+ query = db.create_query(querystring)
+ for msg in query.search_messages():
+ msg.freeze()
+ if cmd == 'tag':
+ for tag in tags:
+ msg.add_tag(tag.encode(DB_ENC),
+ sync_maildir_flags=sync)
+ if cmd == 'set':
+ msg.remove_all_tags()
+ for tag in tags:
+ msg.add_tag(tag.encode(DB_ENC),
+ sync_maildir_flags=sync)
+ elif cmd == 'untag':
+ for tag in tags:
+ msg.remove_tag(tag.encode(DB_ENC),
+ sync_maildir_flags=sync)
+ msg.thaw()
+
+ logging.debug('ended atomic')
+ # end transaction and reinsert queue item on error
+ if db.end_atomic() != notmuch.STATUS.SUCCESS:
+ raise DatabaseError('end_atomic failed')
+ logging.debug('ended atomic')
+
+ # close db
+ db.close()
+ logging.debug('closed db')
+
+ # call post-callback
+ if callable(afterwards):
+ logging.debug(str(afterwards))
+ afterwards()
+ logging.debug('called callback')
+
+ # re-insert item to the queue upon Xapian/NotmuchErrors
+ except (XapianError, NotmuchError) as e:
+ logging.exception(e)
+ self.writequeue.appendleft(current_item)
+ raise DatabaseError(unicode(e))
+ except DatabaseLockedError as e:
+ logging.debug('index temporarily locked')
+ self.writequeue.appendleft(current_item)
+ raise e
+ logging.debug('flush finished')
+
+ def kill_search_processes(self):
+ """
+ terminate all search processes that originate from
+ this managers :meth:`get_threads`.
+ """
+ for p in self.processes:
+ p.terminate()
+ self.processes = []
+
+ def tag(self, querystring, tags, afterwards=None, remove_rest=False):
+ """
+ add tags to messages matching `querystring`.
+ This appends a tag operation to the write queue and raises
+ :exc:`~errors.DatabaseROError` if in read only mode.
+
+ :param querystring: notmuch search string
+ :type querystring: str
+ :param tags: a list of tags to be added
+ :type tags: list of str
+ :param afterwards: callback that gets called after successful
+ application of this tagging operation
+ :type afterwards: callable
+ :param remove_rest: remove tags from matching messages before tagging
+ :type remove_rest: bool
+ :exception: :exc:`~errors.DatabaseROError`
+
+ .. note::
+ This only adds the requested operation to the write queue.
+ You need to call :meth:`DBManager.flush` to actually write out.
+ """
+ if self.ro:
+ raise DatabaseROError()
+ if remove_rest:
+ self.writequeue.append(('set', afterwards, querystring, tags))
+ else:
+ self.writequeue.append(('tag', afterwards, querystring, tags))
+
+ def untag(self, querystring, tags, afterwards=None):
+ """
+ removes tags from messages that match `querystring`.
+ This appends an untag operation to the write queue and raises
+ :exc:`~errors.DatabaseROError` if in read only mode.
+
+ :param querystring: notmuch search string
+ :type querystring: str
+ :param tags: a list of tags to be added
+ :type tags: list of str
+ :param afterwards: callback that gets called after successful
+ application of this tagging operation
+ :type afterwards: callable
+ :exception: :exc:`~errors.DatabaseROError`
+
+ .. note::
+ This only adds the requested operation to the write queue.
+ You need to call :meth:`DBManager.flush` to actually write out.
+ """
+ if self.ro:
+ raise DatabaseROError()
+ self.writequeue.append(('untag', afterwards, querystring, tags))
+
+ def count_messages(self, querystring):
+ """returns number of messages that match `querystring`"""
+ return self.query(querystring).count_messages()
+
+ def count_threads(self, querystring):
+ """returns number of threads that match `querystring`"""
+ return self.query(querystring).count_threads()
+
+ def search_thread_ids(self, querystring):
+ """
+ returns the ids of all threads that match the `querystring`
+ This copies! all integer thread ids into an new list.
+
+ :returns: list of str
+ """
+
+ return self.query_threaded(querystring)
+
+ def _get_notmuch_thread(self, tid):
+ """returns :class:`notmuch.database.Thread` with given id"""
+ query = self.query('thread:' + tid)
+ try:
+ return query.search_threads().next()
+ except StopIteration:
+ errmsg = 'no thread with id %s exists!' % tid
+ raise NonexistantObjectError(errmsg)
+
+ def get_thread(self, tid):
+ """returns :class:`Thread` with given thread id (str)"""
+ return Thread(self, self._get_notmuch_thread(tid))
+
+ def _get_notmuch_message(self, mid):
+ """returns :class:`notmuch.database.Message` with given id"""
+ mode = Database.MODE.READ_ONLY
+ db = Database(path=self.path, mode=mode)
+ try:
+ return db.find_message(mid)
+ except:
+ errmsg = 'no message with id %s exists!' % mid
+ raise NonexistantObjectError(errmsg)
+
+ def get_message(self, mid):
+ """returns :class:`Message` with given message id (str)"""
+ return Message(self, self._get_notmuch_message(mid))
+
+ def get_all_tags(self):
+ """
+ returns all tagsstrings used in the database
+ :rtype: list of str
+ """
+ db = Database(path=self.path)
+ return [t for t in db.get_all_tags()]
+
+ def async(self, cbl, fun):
+ """
+ return a pair (pipe, process) so that the process writes
+ `fun(a)` to the pipe for each element `a` in the iterable returned
+ by the callable `cbl`.
+
+ :param cbl: a function returning something iterable
+ :type cbl: callable
+ :param fun: an unary translation function
+ :type fun: callable
+ :rtype: (:class:`multiprocessing.Pipe`,
+ :class:`multiprocessing.Process`)
+ """
+ pipe = multiprocessing.Pipe(False)
+ receiver, sender = pipe
+ process = FillPipeProcess(cbl(), pipe, fun)
+ process.start()
+ self.processes.append(process)
+ # closing the sending end in this (receiving) process guarantees
+ # that here the apropriate EOFError is raised upon .recv in the walker
+ sender.close()
+ return receiver, process
+
+ def get_threads(self, querystring, sort='newest_first'):
+ """
+ asynchronously look up thread ids matching `querystring`.
+
+ :param querystring: The query string to use for the lookup
+ :type querystring: str.
+ :param sort: Sort order. one of ['oldest_first', 'newest_first',
+ 'message_id', 'unsorted']
+ :type query: str
+ :returns: a pipe together with the process that asynchronously
+ writes to it.
+ :rtype: (:class:`multiprocessing.Pipe`,
+ :class:`multiprocessing.Process`)
+ """
+ assert sort in self._sort_orders.keys()
+ q = self.query(querystring)
+ q.set_sort(self._sort_orders[sort])
+ return self.async(q.search_threads, (lambda a: a.get_thread_id()))
+
+ def query(self, querystring):
+ """
+ creates :class:`notmuch.Query` objects on demand
+
+ :param querystring: The query string to use for the lookup
+ :type query: str.
+ :returns: :class:`notmuch.Query` -- the query object.
+ """
+ mode = Database.MODE.READ_ONLY
+ db = Database(path=self.path, mode=mode)
+ return db.create_query(querystring)
+
+ def add_message(self, path, tags=[], afterwards=None):
+ """
+ Adds a file to the notmuch index.
+
+ :param path: path to the file
+ :type path: str
+ :param tags: tagstrings to add
+ :type tags: list of str
+ :param afterwards: callback to trigger after adding
+ :type afterwards: callable or None
+ """
+ if self.ro:
+ raise DatabaseROError()
+ self.writequeue.append(('add', afterwards, path, tags))
+
+ def remove_message(self, message, afterwards=None):
+ """
+ Remove a message from the notmuch index
+
+ :param message: message to remove
+ :type message: :class:`Message`
+ :param afterwards: callback to trigger after removing
+ :type afterwards: callable or None
+ """
+ if self.ro:
+ raise DatabaseROError()
+ path = message.get_filename()
+ self.writequeue.append(('remove', afterwards, path))
diff --git a/alot/init.py b/alot/init.py
index aaa2f22c..449eea30 100755
--- a/alot/init.py
+++ b/alot/init.py
@@ -5,13 +5,13 @@ import sys
import logging
import os
-from settings import settings, ConfigError
-from db import DBManager
-from ui import UI
+from alot.settings import settings
+from alot.settings.errors import ConfigError
+from alot.db.manager import DBManager
+from alot.ui import UI
import alot.commands as commands
-from commands import *
+from alot.commands import *
from alot.commands import CommandParseError
-import alot
from twisted.python import usage
diff --git a/alot/settings/__init__.py b/alot/settings/__init__.py
index a92eaf0e..57c351b7 100644
--- a/alot/settings/__init__.py
+++ b/alot/settings/__init__.py
@@ -1,398 +1,6 @@
# Copyright (C) 2011-2012 Patrick Totzke <patricktotzke@gmail.com>
# This file is released under the GNU GPL, version 3 or a later revision.
# For further details see the COPYING file
-import imp
-import os
-import re
-import errno
-import mailcap
-import logging
-import shutil
-from configobj import ConfigObj, Section
-
-from alot.account import SendmailAccount
-from alot.addressbooks import MatchSdtoutAddressbook, AbookAddressBook
-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
-
-
-DEFAULTSPATH = os.path.join(os.path.dirname(__file__), '..', 'defaults')
-
-
-class SettingsManager(object):
- """Organizes user settings"""
- def __init__(self, alot_rc=None, notmuch_rc=None, theme=None):
- """
- :param alot_rc: path to alot's config file
- :type alot_rc: str
- :param notmuch_rc: path to notmuch's config file
- :type notmuch_rc: str
- :theme: path to initially used theme file
- :type theme: str
- """
- self.hooks = None
- self._mailcaps = mailcap.getcaps()
-
- theme_path = theme or os.path.join(DEFAULTSPATH, 'default.theme')
- self._theme = Theme(theme_path)
- bindings_path = os.path.join(DEFAULTSPATH, 'default.bindings')
- self._bindings = ConfigObj(bindings_path)
- self._config = ConfigObj()
- self._accounts = None
- self._accountmap = None
- self.read_config(alot_rc)
- self.read_notmuch_config(notmuch_rc)
-
- def read_notmuch_config(self, path):
- """parse notmuch's config file from path"""
- spec = os.path.join(DEFAULTSPATH, 'notmuch.rc.spec')
- self._notmuchconfig = read_config(path, spec)
-
- def read_config(self, path):
- """parse alot's config file from path"""
- spec = os.path.join(DEFAULTSPATH, 'alot.rc.spec')
- newconfig = read_config(path, spec,
- checks={'mail_container': mail_container,
- 'force_list': force_list,
- 'align': align_mode,
- 'attrtriple': attr_triple,
- 'gpg_key_hint': gpg_key})
- self._config.merge(newconfig)
-
- hooks_path = os.path.expanduser(self._config.get('hooksfile'))
- try:
- self.hooks = imp.load_source('hooks', hooks_path)
- except:
- logging.debug('unable to load hooks file:%s' % hooks_path)
- if 'bindings' in newconfig:
- newbindings = newconfig['bindings']
- if isinstance(newbindings, Section):
- self._bindings.merge(newbindings)
- # themes
- themestring = newconfig['theme']
- themes_dir = self._config.get('themes_dir')
- if themes_dir:
- themes_dir = os.path.expanduser(themes_dir)
- else:
- themes_dir = os.path.join(os.environ.get('XDG_CONFIG_HOME',
- os.path.expanduser('~/.config')), 'alot', 'themes')
- logging.debug(themes_dir)
-
- if themestring:
- if not os.path.isdir(themes_dir):
- err_msg = 'cannot find theme %s: themes_dir %s is missing'
- raise ConfigError(err_msg % (themestring, themes_dir))
- else:
- theme_path = os.path.join(themes_dir, themestring)
- 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)
-
- def _parse_accounts(self, config):
- """
- read accounts information from config
-
- :param config: valit alot config
- :type config: `configobj.ConfigObj`
- :returns: list of accounts
- """
- accounts = []
- if 'accounts' in config:
- for acc in config['accounts'].sections:
- accsec = config['accounts'][acc]
- args = dict(config['accounts'][acc])
-
- # create abook for this account
- abook = accsec['abook']
- logging.debug('abook defined: %s' % abook)
- if abook['type'] == 'shellcommand':
- cmd = abook['command']
- regexp = abook['regexp']
- if cmd is not None and regexp is not None:
- args['abook'] = MatchSdtoutAddressbook(cmd,
- match=regexp)
- else:
- msg = 'underspecified abook of type \'shellcommand\':'
- msg += '\ncommand: %s\nregexp:%s' % (cmd, regexp)
- raise ConfigError(msg)
- elif abook['type'] == 'abook':
- contacts_path = abook['abook_contacts_file']
- args['abook'] = AbookAddressBook(contacts_path)
- else:
- del(args['abook'])
-
- cmd = args['sendmail_command']
- del(args['sendmail_command'])
- newacc = SendmailAccount(cmd, **args)
- accounts.append(newacc)
- return accounts
-
- def _account_table(self, accounts):
- """
- creates a lookup table (emailaddress -> account) for a given list of
- accounts
-
- :param accounts: list of accounts
- :type accounts: list of `alot.account.Account`
- :returns: hashtable
- :rvalue: dict (str -> `alot.account.Account`)
- """
- accountmap = {}
- for acc in accounts:
- accountmap[acc.address] = acc
- for alias in acc.aliases:
- accountmap[alias] = acc
- return accountmap
-
- def get(self, key, fallback=None):
- """
- look up global config values from alot's config
-
- :param key: key to look up
- :type key: str
- :param fallback: fallback returned if key is not present
- :type fallback: str
- :returns: config value with type as specified in the spec-file
- """
- value = None
- if key in self._config:
- value = self._config[key]
- if isinstance(value, Section):
- value = None
- if value == None:
- value = fallback
- return value
-
- def set(self, key, value):
- """
- setter for global config values
-
- :param key: config option identifise
- :type key: str
- :param value: option to set
- :type value: depends on the specfile :file:`alot.rc.spec`
- """
- self._config[key] = value
-
- def get_notmuch_setting(self, section, key, fallback=None):
- """
- look up config values from notmuch's config
-
- :param section: key is in
- :type section: str
- :param key: key to look up
- :type key: str
- :param fallback: fallback returned if key is not present
- :type fallback: str
- :returns: config value with type as specified in the spec-file
- """
- value = None
- if section in self._notmuchconfig:
- if key in self._notmuchconfig[section]:
- value = self._notmuchconfig[section][key]
- if value == None:
- value = fallback
- return value
-
- def get_theming_attribute(self, mode, name, part=None):
- """
- looks up theming attribute
-
- :param mode: ui-mode (e.g. `search`,`thread`...)
- :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(colours, mode, name, part)
-
- def get_threadline_theming(self, thread):
- """
- 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.
-
- :param thread: thread to theme
- :type thread: alot.db.thread.Thread
- """
- colours = int(self._config.get('colourmode'))
- return self._theme.get_threadline_theming(thread, colours)
-
- def get_tagstring_representation(self, tag, onebelow_normal=None,
- onebelow_focus=None):
- """
- looks up user's preferred way to represent a given tagstring.
-
- :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 = fallback_normal
- focus = fallback_focus
- translated = tag
-
- return {'normal': normal, 'focussed': focus, 'translated': translated}
-
- def get_hook(self, key):
- """return hook (`callable`) identified by `key`"""
- if self.hooks:
- if key in self.hooks.__dict__:
- return self.hooks.__dict__[key]
- return None
-
- def get_keybinding(self, mode, key):
- """look up keybinding from `MODE-maps` sections
-
- :param mode: mode identifier
- :type mode: str
- :param key: urwid-style key identifier
- :type key: str
- :returns: a command line to be applied upon keypress
- :rtype: str
- """
- cmdline = None
- bindings = self._bindings
- if key in bindings.scalars:
- cmdline = bindings[key]
- if mode in bindings.sections:
- if key in bindings[mode].scalars:
- value = bindings[mode][key]
- if value:
- cmdline = value
- return cmdline
-
- def get_accounts(self):
- """
- returns known accounts
-
- :rtype: list of :class:`Account`
- """
- return self._accounts
-
- def get_account_by_address(self, address):
- """
- returns :class:`Account` for a given email address (str)
-
- :param address: address to look up
- :type address: string
- :rtype: :class:`Account` or None
- """
-
- for myad in self.get_addresses():
- if myad in address:
- return self._accountmap[myad]
- return None
-
- def get_main_addresses(self):
- """returns addresses of known accounts without its aliases"""
- return [a.address for a in self._accounts]
-
- def get_addresses(self):
- """returns addresses of known accounts including all their aliases"""
- return self._accountmap.keys()
-
- def get_addressbooks(self, order=[], append_remaining=True):
- """returns list of all defined :class:`AddressBook` objects"""
- abooks = []
- for a in order:
- if a:
- if a.abook:
- abooks.append(a.abook)
- if append_remaining:
- for a in self._accounts:
- if a.abook and a.abook not in abooks:
- abooks.append(a.abook)
- return abooks
-
- def mailcap_find_match(self, *args, **kwargs):
- """
- Propagates :func:`mailcap.find_match` but caches the mailcap (first
- argument)
- """
- return mailcap.findmatch(self._mailcaps, *args, **kwargs)
-
- def represent_datetime(self, d):
- """
- turns a given datetime obj into a unicode string representation.
- This will:
-
- 1) look if a fixed 'timestamp_format' is given in the config
- 2) check if a 'timestamp_format' hook is defined
- 3) use :func:`~alot.helper.pretty_datetime` as fallback
- """
-
- fixed_format = self.get('timestamp_format')
- if fixed_format:
- rep = string_decode(d.strftime(fixed_format), 'UTF-8')
- else:
- format_hook = self.get_hook('timestamp_format')
- if format_hook:
- rep = string_decode(format_hook(d), 'UTF-8')
- else:
- rep = pretty_datetime(d)
- return rep
+from alot.settings.manager import SettingsManager
settings = SettingsManager()
diff --git a/alot/settings/manager.py b/alot/settings/manager.py
new file mode 100644
index 00000000..c336071c
--- /dev/null
+++ b/alot/settings/manager.py
@@ -0,0 +1,394 @@
+# Copyright (C) 2011-2012 Patrick Totzke <patricktotzke@gmail.com>
+# This file is released under the GNU GPL, version 3 or a later revision.
+# For further details see the COPYING file
+import imp
+import os
+import re
+import mailcap
+import logging
+from configobj import ConfigObj, Section
+
+from alot.account import SendmailAccount
+from alot.addressbooks import MatchSdtoutAddressbook, AbookAddressBook
+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
+
+
+DEFAULTSPATH = os.path.join(os.path.dirname(__file__), '..', 'defaults')
+
+
+class SettingsManager(object):
+ """Organizes user settings"""
+ def __init__(self, alot_rc=None, notmuch_rc=None, theme=None):
+ """
+ :param alot_rc: path to alot's config file
+ :type alot_rc: str
+ :param notmuch_rc: path to notmuch's config file
+ :type notmuch_rc: str
+ :theme: path to initially used theme file
+ :type theme: str
+ """
+ self.hooks = None
+ self._mailcaps = mailcap.getcaps()
+
+ theme_path = theme or os.path.join(DEFAULTSPATH, 'default.theme')
+ self._theme = Theme(theme_path)
+ bindings_path = os.path.join(DEFAULTSPATH, 'default.bindings')
+ self._bindings = ConfigObj(bindings_path)
+ self._config = ConfigObj()
+ self._accounts = None
+ self._accountmap = None
+ self.read_config(alot_rc)
+ self.read_notmuch_config(notmuch_rc)
+
+ def read_notmuch_config(self, path):
+ """parse notmuch's config file from path"""
+ spec = os.path.join(DEFAULTSPATH, 'notmuch.rc.spec')
+ self._notmuchconfig = read_config(path, spec)
+
+ def read_config(self, path):
+ """parse alot's config file from path"""
+ spec = os.path.join(DEFAULTSPATH, 'alot.rc.spec')
+ newconfig = read_config(path, spec,
+ checks={'mail_container': mail_container,
+ 'force_list': force_list,
+ 'align': align_mode,
+ 'attrtriple': attr_triple,
+ 'gpg_key_hint': gpg_key})
+ self._config.merge(newconfig)
+
+ hooks_path = os.path.expanduser(self._config.get('hooksfile'))
+ try:
+ self.hooks = imp.load_source('hooks', hooks_path)
+ except:
+ logging.debug('unable to load hooks file:%s' % hooks_path)
+ if 'bindings' in newconfig:
+ newbindings = newconfig['bindings']
+ if isinstance(newbindings, Section):
+ self._bindings.merge(newbindings)
+ # themes
+ themestring = newconfig['theme']
+ themes_dir = self._config.get('themes_dir')
+ if themes_dir:
+ themes_dir = os.path.expanduser(themes_dir)
+ else:
+ themes_dir = os.path.join(os.environ.get('XDG_CONFIG_HOME',
+ os.path.expanduser('~/.config')), 'alot', 'themes')
+ logging.debug(themes_dir)
+
+ if themestring:
+ if not os.path.isdir(themes_dir):
+ err_msg = 'cannot find theme %s: themes_dir %s is missing'
+ raise ConfigError(err_msg % (themestring, themes_dir))
+ else:
+ theme_path = os.path.join(themes_dir, themestring)
+ 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)
+
+ def _parse_accounts(self, config):
+ """
+ read accounts information from config
+
+ :param config: valit alot config
+ :type config: `configobj.ConfigObj`
+ :returns: list of accounts
+ """
+ accounts = []
+ if 'accounts' in config:
+ for acc in config['accounts'].sections:
+ accsec = config['accounts'][acc]
+ args = dict(config['accounts'][acc])
+
+ # create abook for this account
+ abook = accsec['abook']
+ logging.debug('abook defined: %s' % abook)
+ if abook['type'] == 'shellcommand':
+ cmd = abook['command']
+ regexp = abook['regexp']
+ if cmd is not None and regexp is not None:
+ args['abook'] = MatchSdtoutAddressbook(cmd,
+ match=regexp)
+ else:
+ msg = 'underspecified abook of type \'shellcommand\':'
+ msg += '\ncommand: %s\nregexp:%s' % (cmd, regexp)
+ raise ConfigError(msg)
+ elif abook['type'] == 'abook':
+ contacts_path = abook['abook_contacts_file']
+ args['abook'] = AbookAddressBook(contacts_path)
+ else:
+ del(args['abook'])
+
+ cmd = args['sendmail_command']
+ del(args['sendmail_command'])
+ newacc = SendmailAccount(cmd, **args)
+ accounts.append(newacc)
+ return accounts
+
+ def _account_table(self, accounts):
+ """
+ creates a lookup table (emailaddress -> account) for a given list of
+ accounts
+
+ :param accounts: list of accounts
+ :type accounts: list of `alot.account.Account`
+ :returns: hashtable
+ :rvalue: dict (str -> `alot.account.Account`)
+ """
+ accountmap = {}
+ for acc in accounts:
+ accountmap[acc.address] = acc
+ for alias in acc.aliases:
+ accountmap[alias] = acc
+ return accountmap
+
+ def get(self, key, fallback=None):
+ """
+ look up global config values from alot's config
+
+ :param key: key to look up
+ :type key: str
+ :param fallback: fallback returned if key is not present
+ :type fallback: str
+ :returns: config value with type as specified in the spec-file
+ """
+ value = None
+ if key in self._config:
+ value = self._config[key]
+ if isinstance(value, Section):
+ value = None
+ if value is None:
+ value = fallback
+ return value
+
+ def set(self, key, value):
+ """
+ setter for global config values
+
+ :param key: config option identifise
+ :type key: str
+ :param value: option to set
+ :type value: depends on the specfile :file:`alot.rc.spec`
+ """
+ self._config[key] = value
+
+ def get_notmuch_setting(self, section, key, fallback=None):
+ """
+ look up config values from notmuch's config
+
+ :param section: key is in
+ :type section: str
+ :param key: key to look up
+ :type key: str
+ :param fallback: fallback returned if key is not present
+ :type fallback: str
+ :returns: config value with type as specified in the spec-file
+ """
+ value = None
+ if section in self._notmuchconfig:
+ if key in self._notmuchconfig[section]:
+ value = self._notmuchconfig[section][key]
+ if value is None:
+ value = fallback
+ return value
+
+ def get_theming_attribute(self, mode, name, part=None):
+ """
+ looks up theming attribute
+
+ :param mode: ui-mode (e.g. `search`,`thread`...)
+ :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(colours, mode, name, part)
+
+ def get_threadline_theming(self, thread):
+ """
+ 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.
+
+ :param thread: thread to theme
+ :type thread: alot.db.thread.Thread
+ """
+ colours = int(self._config.get('colourmode'))
+ return self._theme.get_threadline_theming(thread, colours)
+
+ def get_tagstring_representation(self, tag, onebelow_normal=None,
+ onebelow_focus=None):
+ """
+ looks up user's preferred way to represent a given tagstring.
+
+ :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 = fallback_normal
+ focus = fallback_focus
+ translated = tag
+
+ return {'normal': normal, 'focussed': focus, 'translated': translated}
+
+ def get_hook(self, key):
+ """return hook (`callable`) identified by `key`"""
+ if self.hooks:
+ if key in self.hooks.__dict__:
+ return self.hooks.__dict__[key]
+ return None
+
+ def get_keybinding(self, mode, key):
+ """look up keybinding from `MODE-maps` sections
+
+ :param mode: mode identifier
+ :type mode: str
+ :param key: urwid-style key identifier
+ :type key: str
+ :returns: a command line to be applied upon keypress
+ :rtype: str
+ """
+ cmdline = None
+ bindings = self._bindings
+ if key in bindings.scalars:
+ cmdline = bindings[key]
+ if mode in bindings.sections:
+ if key in bindings[mode].scalars:
+ value = bindings[mode][key]
+ if value:
+ cmdline = value
+ return cmdline
+
+ def get_accounts(self):
+ """
+ returns known accounts
+
+ :rtype: list of :class:`Account`
+ """
+ return self._accounts
+
+ def get_account_by_address(self, address):
+ """
+ returns :class:`Account` for a given email address (str)
+
+ :param address: address to look up
+ :type address: string
+ :rtype: :class:`Account` or None
+ """
+
+ for myad in self.get_addresses():
+ if myad in address:
+ return self._accountmap[myad]
+ return None
+
+ def get_main_addresses(self):
+ """returns addresses of known accounts without its aliases"""
+ return [a.address for a in self._accounts]
+
+ def get_addresses(self):
+ """returns addresses of known accounts including all their aliases"""
+ return self._accountmap.keys()
+
+ def get_addressbooks(self, order=[], append_remaining=True):
+ """returns list of all defined :class:`AddressBook` objects"""
+ abooks = []
+ for a in order:
+ if a:
+ if a.abook:
+ abooks.append(a.abook)
+ if append_remaining:
+ for a in self._accounts:
+ if a.abook and a.abook not in abooks:
+ abooks.append(a.abook)
+ return abooks
+
+ def mailcap_find_match(self, *args, **kwargs):
+ """
+ Propagates :func:`mailcap.find_match` but caches the mailcap (first
+ argument)
+ """
+ return mailcap.findmatch(self._mailcaps, *args, **kwargs)
+
+ def represent_datetime(self, d):
+ """
+ turns a given datetime obj into a unicode string representation.
+ This will:
+
+ 1) look if a fixed 'timestamp_format' is given in the config
+ 2) check if a 'timestamp_format' hook is defined
+ 3) use :func:`~alot.helper.pretty_datetime` as fallback
+ """
+
+ fixed_format = self.get('timestamp_format')
+ if fixed_format:
+ rep = string_decode(d.strftime(fixed_format), 'UTF-8')
+ else:
+ format_hook = self.get_hook('timestamp_format')
+ if format_hook:
+ rep = string_decode(format_hook(d), 'UTF-8')
+ else:
+ rep = pretty_datetime(d)
+ return rep
diff --git a/alot/ui.py b/alot/ui.py
index ddb53798..0f07437f 100644
--- a/alot/ui.py
+++ b/alot/ui.py
@@ -11,7 +11,9 @@ import commands
from commands import commandfactory
from alot.commands import CommandParseError
from alot.helper import string_decode
-import widgets
+from alot.widgets.utils import CatchKeyWidgetWrap
+from alot.widgets.globals import CompleteEdit
+from alot.widgets.globals import ChoiceWidget
class InputWrap(urwid.WidgetWrap):
@@ -92,9 +94,9 @@ class UI(object):
self.mainframe_themed = urwid.AttrMap(self.mainframe, global_att)
self.inputwrap = InputWrap(self, self.mainframe_themed)
self.mainloop = urwid.MainLoop(self.inputwrap,
- handle_mouse=False,
- event_loop=urwid.TwistedEventLoop(),
- unhandled_input=self.unhandeled_input)
+ handle_mouse=False,
+ event_loop=urwid.TwistedEventLoop(),
+ unhandled_input=self.unhandeled_input)
self.mainloop.screen.set_terminal_properties(colors=colourmode)
self.show_statusbar = settings.get('show_statusbar')
@@ -123,8 +125,8 @@ class UI(object):
logging.debug('called')
afterwards()
logging.debug('relay: %s' % relay_rest)
- helpwrap = widgets.CatchKeyWidgetWrap(w, key, on_catch=oe,
- relay_rest=relay_rest)
+ helpwrap = CatchKeyWidgetWrap(w, key, on_catch=oe,
+ relay_rest=relay_rest)
self.inputwrap.set_root(helpwrap)
self.inputwrap.select_cancel_only = not relay_rest
@@ -158,7 +160,7 @@ class UI(object):
#set up widgets
leftpart = urwid.Text(prefix, align='left')
- editpart = widgets.CompleteEdit(completer, on_exit=select_or_cancel,
+ editpart = CompleteEdit(completer, on_exit=select_or_cancel,
edit_text=text, history=history)
for i in range(tab): # hit some tabs
@@ -314,8 +316,8 @@ class UI(object):
#set up widgets
msgpart = urwid.Text(message)
- choicespart = widgets.ChoiceWidget(choices, callback=select_or_cancel,
- select=select, cancel=cancel)
+ choicespart = ChoiceWidget(choices, callback=select_or_cancel,
+ select=select, cancel=cancel)
# build widget
if msg_position == 'left':
diff --git a/alot/widgets.py b/alot/widgets.py
deleted file mode 100644
index 16d4b53b..00000000
--- a/alot/widgets.py
+++ /dev/null
@@ -1,738 +0,0 @@
-# Copyright (C) 2011-2012 Patrick Totzke <patricktotzke@gmail.com>
-# This file is released under the GNU GPL, version 3 or a later revision.
-# For further details see the COPYING file
-import urwid
-import logging
-
-from settings import settings
-from alot.helper import shorten_author_string
-from alot.helper import tag_cmp
-from alot.helper import string_decode
-import alot.db.message as message
-from alot.db.attachment import Attachment
-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)
- self.title = urwid.Text(title)
- if titleattr is not None:
- self.title = urwid.AttrMap(self.title, titleattr)
- if bodyattr is not None:
- self.body = urwid.AttrMap(self.body, bodyattr)
-
- box = urwid.Overlay(self.title, self.body,
- align='center',
- valign='top',
- width=len(title),
- height=None,
- )
- urwid.WidgetWrap.__init__(self, box)
-
- def selectable(self):
- return self.body.selectable()
-
- def keypress(self, size, key):
- return self.body.keypress(size, key)
-
-
-class CatchKeyWidgetWrap(urwid.WidgetWrap):
- def __init__(self, widget, key, on_catch, relay_rest=True):
- urwid.WidgetWrap.__init__(self, widget)
- self.key = key
- self.relay = relay_rest
- self.on_catch = on_catch
-
- def selectable(self):
- return True
-
- def keypress(self, size, key):
- logging.debug('CATCH KEY: %s' % key)
- logging.debug('relay: %s' % self.relay)
- if key == self.key:
- self.on_catch()
- elif self._w.selectable() and self.relay:
- return self._w.keypress(size, key)
-
-
-class ThreadlineWidget(urwid.AttrMap):
- """
- selectable line widget that represents a :class:`~alot.db.Thread`
- in the :class:`~alot.buffers.SearchBuffer`.
- """
- def __init__(self, tid, dbman):
- self.dbman = dbman
- self.thread = dbman.get_thread(tid)
- self.tag_widgets = []
- self.display_content = settings.get('display_content_in_threadline')
- self.structure = None
- self.rebuild()
- normal = self.structure['normal']
- focussed = self.structure['focus']
- urwid.AttrMap.__init__(self, self.columns, normal, focussed)
-
- 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
- datestring = ''
- 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'])
-
- 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:
- msgs = []
- # 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 = pad(lastcontent.replace('\n', ' ').strip())
- content_w = AttrFlipWidget(urwid.Text(
- contentstring,
- wrap='clip'),
- 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
-
- 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):
- for w in self.widgets:
- w.set_map('focus' if focus else 'normal')
- return urwid.AttrMap.render(self, size, focus)
-
- def selectable(self):
- return True
-
- def keypress(self, size, key):
- return key
-
- def get_thread(self):
- return self.thread
-
- def _get_theme(self, component, focus=False):
- path = ['search', 'threadline', component]
- if focus:
- path.append('focus')
- else:
- path.append('normal')
- return settings.get_theming_attribute(path)
-
-
-class BufferlineWidget(urwid.Text):
- """
- selectable text widget that represents a :class:`~alot.buffers.Buffer`
- in the :class:`~alot.buffers.BufferlistBuffer`.
- """
-
- def __init__(self, buffer):
- self.buffer = buffer
- line = buffer.__str__()
- urwid.Text.__init__(self, line, wrap='clip')
-
- def selectable(self):
- return True
-
- def keypress(self, size, key):
- return key
-
- def get_buffer(self):
- return self.buffer
-
-
-class TagWidget(urwid.AttrMap):
- """
- text widget that renders a tagstring.
-
- 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, fallback_normal=None, fallback_focus=None):
- self.tag = tag
- 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')
- 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
- # lead e.g. to strings with length 1 that need width 2
- return self.txt.pack()[0]
-
- def selectable(self):
- return True
-
- def keypress(self, size, key):
- return key
-
- def get_tag(self):
- return self.tag
-
- def set_focussed(self):
- self.set_attr_map(self.attmap['focus'])
-
- def set_unfocussed(self):
- self.set_attr_map(self.attmap['normal'])
-
-
-class ChoiceWidget(urwid.Text):
- def __init__(self, choices, callback, cancel=None, select=None):
- self.choices = choices
- self.callback = callback
- self.cancel = cancel
- self.select = select
-
- items = []
- for k, v in choices.items():
- if v == select and select != None:
- items.append('[%s]:%s' % (k, v))
- else:
- items.append('(%s):%s' % (k, v))
- urwid.Text.__init__(self, ' '.join(items))
-
- def selectable(self):
- return True
-
- def keypress(self, size, key):
- if key == 'select' and self.select != None:
- self.callback(self.select)
- elif key == 'cancel' and self.cancel != None:
- self.callback(self.cancel)
- elif key in self.choices:
- self.callback(self.choices[key])
- else:
- return key
-
-
-class CompleteEdit(urwid.Edit):
- def __init__(self, completer, on_exit, edit_text=u'',
- history=None, **kwargs):
- self.completer = completer
- self.on_exit = on_exit
- self.history = list(history) # we temporarily add stuff here
- self.historypos = None
-
- if not isinstance(edit_text, unicode):
- edit_text = string_decode(edit_text)
- self.start_completion_pos = len(edit_text)
- self.completions = None
- urwid.Edit.__init__(self, edit_text=edit_text, **kwargs)
-
- def keypress(self, size, key):
- # if we tabcomplete
- if key in ['tab', 'shift tab'] and self.completer:
- # if not already in completion mode
- if not self.completions:
- self.completions = [(self.edit_text, self.edit_pos)] + \
- self.completer.complete(self.edit_text, self.edit_pos)
- self.focus_in_clist = 1
- else: # otherwise tab through results
- if key == 'tab':
- self.focus_in_clist += 1
- else:
- self.focus_in_clist -= 1
- if len(self.completions) > 1:
- ctext, cpos = self.completions[self.focus_in_clist %
- len(self.completions)]
- self.set_edit_text(ctext)
- self.set_edit_pos(cpos)
- else:
- self.edit_pos += 1
- if self.edit_pos >= len(self.edit_text):
- self.edit_text += ' '
- self.completions = None
- elif key in ['up', 'down']:
- if self.history:
- if self.historypos == None:
- self.history.append(self.edit_text)
- self.historypos = len(self.history) - 1
- if key == 'cursor up':
- self.historypos = (self.historypos + 1) % len(self.history)
- else:
- self.historypos = (self.historypos - 1) % len(self.history)
- self.set_edit_text(self.history[self.historypos])
- elif key == 'select':
- self.on_exit(self.edit_text)
- elif key == 'cancel':
- self.on_exit(None)
- elif key == 'ctrl a':
- self.set_edit_pos(0)
- elif key == 'ctrl e':
- self.set_edit_pos(len(self.edit_text))
- else:
- result = urwid.Edit.keypress(self, size, key)
- self.completions = None
- return result
-
-
-class MessageWidget(urwid.WidgetWrap):
- """
- Flow widget that renders a :class:`~alot.db.message.Message`.
- """
- #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
- #(subclass urwid.Pile) and use urwids new Tree widgets
- def __init__(self, message, even=False, folded=True, raw=False,
- all_headers=False, depth=0, bars_at=[]):
- """
- :param message: the message to display
- :type message: alot.db.Message
- :param even: use messagesummary_even theme for summary
- :type even: bool
- :param folded: fold message initially
- :type folded: bool
- :param raw: show message source initially
- :type raw: bool
- :param all_headers: show all headers initially
- :type all_headers: bool
- :param depth: number of characters to shift content to the right
- :type depth: int
- :param bars_at: defines for each column of the indentation whether to
- use a vertical bar instead of a space.
- :type bars_at: list(bool)
- """
- self.message = message
- self.mail = self.message.get_email()
-
- self.depth = depth
- self.bars_at = bars_at
- self.even = even
- self.folded = folded
- self.show_raw = raw
- self.show_all_headers = all_headers
-
- # define subwidgets that will be created on demand
- self.sumline = None
- self.headerw = None
- self.attachmentw = None
- self.bodyw = None
- self.sourcew = None
-
- # set available and to be displayed headers
- self._all_headers = list(set(self.mail.keys()))
- displayed = settings.get('displayed_headers')
- 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)
-
- def get_focus(self):
- return self.pile.get_focus()
-
- def rebuild(self):
- self.sumline = self._build_sum_line()
- if not self.folded: # only if already unfolded
- self.displayed_list = [self.sumline]
- if self.show_raw:
- srcw = self._get_source_widget()
- self.displayed_list.append(srcw)
- else:
- hw = self._get_header_widget()
- aw = self._get_attachment_widget()
- bw = self._get_body_widget()
- if hw:
- self.displayed_list.append(hw)
- if aw:
- self.displayed_list.append(aw)
- self.displayed_list.append(bw)
- else:
- self.displayed_list = [self.sumline]
- self.pile = urwid.Pile(self.displayed_list)
- self._w = self.pile
-
- def _build_sum_line(self):
- """creates/returns the widget that displays the summary line."""
- self.sumw = MessageSummaryWidget(self.message, even=self.even)
- cols = []
- bc = list() # box_columns
- if self.depth > 1:
- bc.append(0)
- spacer = self._get_spacer(self.bars_at[1:-1])
- cols.append(spacer)
- if self.depth > 0:
- if self.bars_at[-1]:
- arrowhead = [(self.arrow_bars_att, u'\u251c'),
- (self.arrow_heads_att, u'\u25b6')]
- else:
- 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)
- return line
-
- def _get_header_widget(self):
- """creates/returns the widget that displays the mail header"""
- all_shown = (self._all_headers == self._displayed_headers)
-
- if self.headerw and (self.show_all_headers == all_shown):
- return self.headerw
-
- if self.show_all_headers:
- self._displayed_headers = self._all_headers
- else:
- self._displayed_headers = self._filtered_headers
-
- mail = self.message.get_email()
- # normalize values if only filtered list is shown
- norm = not (self._displayed_headers == self._all_headers)
-
- #build lines
- lines = []
- for key in self._displayed_headers:
- if key in mail:
- if key.lower() in ['cc', 'bcc', 'to']:
- values = mail.get_all(key)
- 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)
- lines.append((key, dvalue))
-
- key_att = settings.get_theming_attribute('thread', 'header_key')
- value_att = settings.get_theming_attribute('thread', 'header_value')
- cols = [HeadersList(lines, key_att, value_att)]
- bc = list()
- if self.depth:
- cols.insert(0, self._get_spacer(self.bars_at[1:]))
- bc.append(0)
- cols.insert(1, self._get_arrowhead_aligner())
- bc.append(1)
- self.headerw = urwid.Columns(cols, box_columns=bc)
- return self.headerw
-
- def _get_attachment_widget(self):
- if self.message.get_attachments() and not self.attachmentw:
- lines = []
- for a in self.message.get_attachments():
- cols = [AttachmentWidget(a)]
- bc = list()
- if self.depth:
- cols.insert(0, self._get_spacer(self.bars_at[1:]))
- bc.append(0)
- cols.insert(1, self._get_arrowhead_aligner())
- bc.append(1)
- lines.append(urwid.Columns(cols, box_columns=bc))
- self.attachmentw = urwid.Pile(lines)
- return self.attachmentw
-
- def _get_body_widget(self):
- """creates/returns the widget that displays the mail body"""
- if not self.bodyw:
- cols = [MessageBodyWidget(self.message.get_email())]
- bc = list()
- if self.depth:
- cols.insert(0, self._get_spacer(self.bars_at[1:]))
- bc.append(0)
- cols.insert(1, self._get_arrowhead_aligner())
- bc.append(1)
- self.bodyw = urwid.Columns(cols, box_columns=bc)
- return self.bodyw
-
- def _get_source_widget(self):
- """creates/returns the widget that displays the mail body"""
- if not self.sourcew:
- cols = [urwid.Text(self.message.get_email().as_string())]
- bc = list()
- if self.depth:
- cols.insert(0, self._get_spacer(self.bars_at[1:]))
- bc.append(0)
- cols.insert(1, self._get_arrowhead_aligner())
- bc.append(1)
- self.sourcew = urwid.Columns(cols, box_columns=bc)
- return self.sourcew
-
- def _get_spacer(self, bars_at):
- prefixchars = []
- length = len(bars_at)
- for b in bars_at:
- if b:
- c = u'\u2502'
- else:
- c = ' '
- 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):
- if self.message.has_replies():
- aligner = u'\u2502'
- else:
- aligner = ' '
- aligner = urwid.SolidFill(aligner)
- return ('fixed', 1, urwid.AttrMap(aligner, self.arrow_bars_att))
-
- def selectable(self):
- return True
-
- def keypress(self, size, key):
- return self.pile.keypress(size, key)
-
- def get_message(self):
- """get contained :class`~alot.db.message.Message`"""
- return self.message
-
- def get_email(self):
- """get contained :class:`email <email.Message>`"""
- return self.message.get_email()
-
-
-class MessageSummaryWidget(urwid.WidgetWrap):
- """
- one line summary of a :class:`~alot.db.message.Message`.
- """
-
- def __init__(self, message, even=True):
- """
- :param message: a message
- :type message: alot.db.Message
- :param even: even entry in a pile of messages? Used for theming.
- :type even: bool
- """
- self.message = message
- self.even = even
- if even:
- attr = settings.get_theming_attribute('thread', 'summary', 'even')
- else:
- attr = settings.get_theming_attribute('thread', 'summary', 'odd')
- focus_att = settings.get_theming_attribute('thread', 'summary',
- 'focus')
- cols = []
-
- sumstr = self.__str__()
- txt = urwid.Text(sumstr)
- cols.append(txt)
-
- thread_tags = message.get_thread().get_tags(intersection=True)
- outstanding_tags = set(message.get_tags()).difference(thread_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))
- line = urwid.AttrMap(urwid.Columns(cols, dividechars=1), attr,
- focus_att)
- urwid.WidgetWrap.__init__(self, line)
-
- def __str__(self):
- author, address = self.message.get_author()
- date = self.message.get_datestring()
- rep = author if author != '' else address
- if date != None:
- rep += " (%s)" % date
- return rep
-
- def selectable(self):
- return True
-
- def keypress(self, size, key):
- return key
-
-
-class HeadersList(urwid.WidgetWrap):
- """ renders a pile of header values as key/value list """
- def __init__(self, headerslist, key_attr, value_attr):
- self.headers = headerslist
- self.key_attr = key_attr
- self.value_attr = value_attr
- pile = urwid.Pile(self._build_lines(headerslist))
- att = settings.get_theming_attribute('thread', 'header')
- pile = urwid.AttrMap(pile, att)
- urwid.WidgetWrap.__init__(self, pile)
-
- def __str__(self):
- return str(self.headers)
-
- def _build_lines(self, lines):
- max_key_len = 1
- headerlines = []
- #calc max length of key-string
- for key, value in lines:
- if len(key) > max_key_len:
- max_key_len = len(key)
- for key, value in lines:
- ##todo : even/odd
- keyw = ('fixed', max_key_len + 1,
- urwid.Text((self.key_attr, key)))
- valuew = urwid.Text((self.value_attr, value))
- line = urwid.Columns([keyw, valuew])
- headerlines.append(line)
- return headerlines
-
-
-class MessageBodyWidget(urwid.AttrMap):
- """
- displays printable parts of an email
- """
-
- def __init__(self, msg):
- bodytxt = message.extract_body(msg)
- att = settings.get_theming_attribute('thread', 'body')
- urwid.AttrMap.__init__(self, urwid.Text(bodytxt), att)
-
-
-class AttachmentWidget(urwid.WidgetWrap):
- """
- one-line summary of an :class:`~alot.db.attachment.Attachment`.
- """
- def __init__(self, attachment, selectable=True):
- self._selectable = selectable
- self.attachment = attachment
- if not isinstance(attachment, Attachment):
- self.attachment = Attachment(self.attachment)
- att = settings.get_theming_attribute('thread', 'attachment')
- focus_att = settings.get_theming_attribute('thread',
- 'attachment_focus')
- widget = urwid.AttrMap(urwid.Text(self.attachment.__str__()),
- att, focus_att)
- urwid.WidgetWrap.__init__(self, widget)
-
- def get_attachment(self):
- return self.attachment
-
- def selectable(self):
- return self._selectable
-
- def keypress(self, size, key):
- return key
diff --git a/alot/widgets/__init__.py b/alot/widgets/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/alot/widgets/__init__.py
diff --git a/alot/widgets/bufferlist.py b/alot/widgets/bufferlist.py
new file mode 100644
index 00000000..0ab315c5
--- /dev/null
+++ b/alot/widgets/bufferlist.py
@@ -0,0 +1,29 @@
+# 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
+
+"""
+Widgets specific to Bufferlist mode
+"""
+import urwid
+
+
+class BufferlineWidget(urwid.Text):
+ """
+ selectable text widget that represents a :class:`~alot.buffers.Buffer`
+ in the :class:`~alot.buffers.BufferlistBuffer`.
+ """
+
+ def __init__(self, buffer):
+ self.buffer = buffer
+ line = buffer.__str__()
+ urwid.Text.__init__(self, line, wrap='clip')
+
+ def selectable(self):
+ return True
+
+ def keypress(self, size, key):
+ return key
+
+ def get_buffer(self):
+ return self.buffer
diff --git a/alot/widgets/globals.py b/alot/widgets/globals.py
new file mode 100644
index 00000000..210d4b54
--- /dev/null
+++ b/alot/widgets/globals.py
@@ -0,0 +1,203 @@
+# 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
+
+"""
+This contains alot-specific :class:`urwid.Widget` used in more than one mode.
+"""
+import urwid
+
+from alot.helper import string_decode
+from alot.settings import settings
+from alot.db.attachment import Attachment
+
+
+class AttachmentWidget(urwid.WidgetWrap):
+ """
+ one-line summary of an :class:`~alot.db.attachment.Attachment`.
+ """
+ def __init__(self, attachment, selectable=True):
+ self._selectable = selectable
+ self.attachment = attachment
+ if not isinstance(attachment, Attachment):
+ self.attachment = Attachment(self.attachment)
+ att = settings.get_theming_attribute('thread', 'attachment')
+ focus_att = settings.get_theming_attribute('thread',
+ 'attachment_focus')
+ widget = urwid.AttrMap(urwid.Text(self.attachment.__str__()),
+ att, focus_att)
+ urwid.WidgetWrap.__init__(self, widget)
+
+ def get_attachment(self):
+ return self.attachment
+
+ def selectable(self):
+ return self._selectable
+
+ def keypress(self, size, key):
+ return key
+
+
+class ChoiceWidget(urwid.Text):
+ def __init__(self, choices, callback, cancel=None, select=None):
+ self.choices = choices
+ self.callback = callback
+ self.cancel = cancel
+ self.select = select
+
+ items = []
+ for k, v in choices.items():
+ if v == select and select is not None:
+ items.append('[%s]:%s' % (k, v))
+ else:
+ items.append('(%s):%s' % (k, v))
+ urwid.Text.__init__(self, ' '.join(items))
+
+ def selectable(self):
+ return True
+
+ def keypress(self, size, key):
+ if key == 'select' and self.select is not None:
+ self.callback(self.select)
+ elif key == 'cancel' and self.cancel is not None:
+ self.callback(self.cancel)
+ elif key in self.choices:
+ self.callback(self.choices[key])
+ else:
+ return key
+
+
+class CompleteEdit(urwid.Edit):
+ def __init__(self, completer, on_exit, edit_text=u'',
+ history=None, **kwargs):
+ self.completer = completer
+ self.on_exit = on_exit
+ self.history = list(history) # we temporarily add stuff here
+ self.historypos = None
+
+ if not isinstance(edit_text, unicode):
+ edit_text = string_decode(edit_text)
+ self.start_completion_pos = len(edit_text)
+ self.completions = None
+ urwid.Edit.__init__(self, edit_text=edit_text, **kwargs)
+
+ def keypress(self, size, key):
+ # if we tabcomplete
+ if key in ['tab', 'shift tab'] and self.completer:
+ # if not already in completion mode
+ if not self.completions:
+ self.completions = [(self.edit_text, self.edit_pos)] + \
+ self.completer.complete(self.edit_text, self.edit_pos)
+ self.focus_in_clist = 1
+ else: # otherwise tab through results
+ if key == 'tab':
+ self.focus_in_clist += 1
+ else:
+ self.focus_in_clist -= 1
+ if len(self.completions) > 1:
+ ctext, cpos = self.completions[self.focus_in_clist %
+ len(self.completions)]
+ self.set_edit_text(ctext)
+ self.set_edit_pos(cpos)
+ else:
+ self.edit_pos += 1
+ if self.edit_pos >= len(self.edit_text):
+ self.edit_text += ' '
+ self.completions = None
+ elif key in ['up', 'down']:
+ if self.history:
+ if self.historypos is None:
+ self.history.append(self.edit_text)
+ self.historypos = len(self.history) - 1
+ if key == 'cursor up':
+ self.historypos = (self.historypos + 1) % len(self.history)
+ else:
+ self.historypos = (self.historypos - 1) % len(self.history)
+ self.set_edit_text(self.history[self.historypos])
+ elif key == 'select':
+ self.on_exit(self.edit_text)
+ elif key == 'cancel':
+ self.on_exit(None)
+ elif key == 'ctrl a':
+ self.set_edit_pos(0)
+ elif key == 'ctrl e':
+ self.set_edit_pos(len(self.edit_text))
+ else:
+ result = urwid.Edit.keypress(self, size, key)
+ self.completions = None
+ return result
+
+
+class HeadersList(urwid.WidgetWrap):
+ """ renders a pile of header values as key/value list """
+ def __init__(self, headerslist, key_attr, value_attr):
+ self.headers = headerslist
+ self.key_attr = key_attr
+ self.value_attr = value_attr
+ pile = urwid.Pile(self._build_lines(headerslist))
+ att = settings.get_theming_attribute('thread', 'header')
+ pile = urwid.AttrMap(pile, att)
+ urwid.WidgetWrap.__init__(self, pile)
+
+ def __str__(self):
+ return str(self.headers)
+
+ def _build_lines(self, lines):
+ max_key_len = 1
+ headerlines = []
+ #calc max length of key-string
+ for key, value in lines:
+ if len(key) > max_key_len:
+ max_key_len = len(key)
+ for key, value in lines:
+ ##todo : even/odd
+ keyw = ('fixed', max_key_len + 1,
+ urwid.Text((self.key_attr, key)))
+ valuew = urwid.Text((self.value_attr, value))
+ line = urwid.Columns([keyw, valuew])
+ headerlines.append(line)
+ return headerlines
+
+
+class TagWidget(urwid.AttrMap):
+ """
+ text widget that renders a tagstring.
+
+ 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, fallback_normal=None, fallback_focus=None):
+ self.tag = tag
+ 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')
+ 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
+ # lead e.g. to strings with length 1 that need width 2
+ return self.txt.pack()[0]
+
+ def selectable(self):
+ return True
+
+ def keypress(self, size, key):
+ return key
+
+ def get_tag(self):
+ return self.tag
+
+ def set_focussed(self):
+ self.set_attr_map(self.attmap['focus'])
+
+ def set_unfocussed(self):
+ self.set_attr_map(self.attmap['normal'])
diff --git a/alot/widgets/search.py b/alot/widgets/search.py
new file mode 100644
index 00000000..6f8ed126
--- /dev/null
+++ b/alot/widgets/search.py
@@ -0,0 +1,186 @@
+# 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
+"""
+Widgets specific to search mode
+"""
+import urwid
+
+from alot.settings import settings
+from alot.helper import shorten_author_string
+from alot.helper import tag_cmp
+from alot.widgets.utils import AttrFlipWidget
+from alot.widgets.globals import TagWidget
+
+
+class ThreadlineWidget(urwid.AttrMap):
+ """
+ selectable line widget that represents a :class:`~alot.db.Thread`
+ in the :class:`~alot.buffers.SearchBuffer`.
+ """
+ def __init__(self, tid, dbman):
+ self.dbman = dbman
+ self.thread = dbman.get_thread(tid)
+ self.tag_widgets = []
+ self.display_content = settings.get('display_content_in_threadline')
+ self.structure = None
+ self.rebuild()
+ normal = self.structure['normal']
+ focussed = self.structure['focus']
+ urwid.AttrMap.__init__(self, self.columns, normal, focussed)
+
+ 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
+ datestring = ''
+ 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'])
+
+ 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:
+ msgs = []
+ # 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 = pad(lastcontent.replace('\n', ' ').strip())
+ content_w = AttrFlipWidget(urwid.Text(
+ contentstring,
+ wrap='clip'),
+ 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
+
+ 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):
+ for w in self.widgets:
+ w.set_map('focus' if focus else 'normal')
+ return urwid.AttrMap.render(self, size, focus)
+
+ def selectable(self):
+ return True
+
+ def keypress(self, size, key):
+ return key
+
+ def get_thread(self):
+ return self.thread
+
+ def _get_theme(self, component, focus=False):
+ path = ['search', 'threadline', component]
+ if focus:
+ path.append('focus')
+ else:
+ path.append('normal')
+ return settings.get_theming_attribute(path)
diff --git a/alot/widgets/thread.py b/alot/widgets/thread.py
new file mode 100644
index 00000000..c6d399e8
--- /dev/null
+++ b/alot/widgets/thread.py
@@ -0,0 +1,301 @@
+# 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
+"""
+Widgets specific to thread mode
+"""
+import urwid
+import logging
+
+from alot.settings import settings
+from alot.db.utils import decode_header
+import alot.db.message as message
+from alot.helper import tag_cmp
+from alot.widgets.globals import HeadersList
+from alot.widgets.globals import TagWidget
+from alot.widgets.globals import AttachmentWidget
+
+
+class MessageWidget(urwid.WidgetWrap):
+ """
+ Flow widget that renders a :class:`~alot.db.message.Message`.
+ """
+ #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
+ #(subclass urwid.Pile) and use urwids new Tree widgets
+ def __init__(self, message, even=False, folded=True, raw=False,
+ all_headers=False, depth=0, bars_at=[]):
+ """
+ :param message: the message to display
+ :type message: alot.db.Message
+ :param even: use messagesummary_even theme for summary
+ :type even: bool
+ :param folded: fold message initially
+ :type folded: bool
+ :param raw: show message source initially
+ :type raw: bool
+ :param all_headers: show all headers initially
+ :type all_headers: bool
+ :param depth: number of characters to shift content to the right
+ :type depth: int
+ :param bars_at: defines for each column of the indentation whether to
+ use a vertical bar instead of a space.
+ :type bars_at: list(bool)
+ """
+ self.message = message
+ self.mail = self.message.get_email()
+
+ self.depth = depth
+ self.bars_at = bars_at
+ self.even = even
+ self.folded = folded
+ self.show_raw = raw
+ self.show_all_headers = all_headers
+
+ # define subwidgets that will be created on demand
+ self.sumline = None
+ self.headerw = None
+ self.attachmentw = None
+ self.bodyw = None
+ self.sourcew = None
+
+ # set available and to be displayed headers
+ self._all_headers = list(set(self.mail.keys()))
+ displayed = settings.get('displayed_headers')
+ 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)
+
+ def get_focus(self):
+ return self.pile.get_focus()
+
+ def rebuild(self):
+ self.sumline = self._build_sum_line()
+ if not self.folded: # only if already unfolded
+ self.displayed_list = [self.sumline]
+ if self.show_raw:
+ srcw = self._get_source_widget()
+ self.displayed_list.append(srcw)
+ else:
+ hw = self._get_header_widget()
+ aw = self._get_attachment_widget()
+ bw = self._get_body_widget()
+ if hw:
+ self.displayed_list.append(hw)
+ if aw:
+ self.displayed_list.append(aw)
+ self.displayed_list.append(bw)
+ else:
+ self.displayed_list = [self.sumline]
+ self.pile = urwid.Pile(self.displayed_list)
+ self._w = self.pile
+
+ def _build_sum_line(self):
+ """creates/returns the widget that displays the summary line."""
+ self.sumw = MessageSummaryWidget(self.message, even=self.even)
+ cols = []
+ bc = list() # box_columns
+ if self.depth > 1:
+ bc.append(0)
+ spacer = self._get_spacer(self.bars_at[1:-1])
+ cols.append(spacer)
+ if self.depth > 0:
+ if self.bars_at[-1]:
+ arrowhead = [(self.arrow_bars_att, u'\u251c'),
+ (self.arrow_heads_att, u'\u25b6')]
+ else:
+ 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)
+ return line
+
+ def _get_header_widget(self):
+ """creates/returns the widget that displays the mail header"""
+ all_shown = (self._all_headers == self._displayed_headers)
+
+ if self.headerw and (self.show_all_headers == all_shown):
+ return self.headerw
+
+ if self.show_all_headers:
+ self._displayed_headers = self._all_headers
+ else:
+ self._displayed_headers = self._filtered_headers
+
+ mail = self.message.get_email()
+ # normalize values if only filtered list is shown
+ norm = not (self._displayed_headers == self._all_headers)
+
+ #build lines
+ lines = []
+ for key in self._displayed_headers:
+ if key in mail:
+ if key.lower() in ['cc', 'bcc', 'to']:
+ values = mail.get_all(key)
+ 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)
+ lines.append((key, dvalue))
+
+ key_att = settings.get_theming_attribute('thread', 'header_key')
+ value_att = settings.get_theming_attribute('thread', 'header_value')
+ cols = [HeadersList(lines, key_att, value_att)]
+ bc = list()
+ if self.depth:
+ cols.insert(0, self._get_spacer(self.bars_at[1:]))
+ bc.append(0)
+ cols.insert(1, self._get_arrowhead_aligner())
+ bc.append(1)
+ self.headerw = urwid.Columns(cols, box_columns=bc)
+ return self.headerw
+
+ def _get_attachment_widget(self):
+ if self.message.get_attachments() and not self.attachmentw:
+ lines = []
+ for a in self.message.get_attachments():
+ cols = [AttachmentWidget(a)]
+ bc = list()
+ if self.depth:
+ cols.insert(0, self._get_spacer(self.bars_at[1:]))
+ bc.append(0)
+ cols.insert(1, self._get_arrowhead_aligner())
+ bc.append(1)
+ lines.append(urwid.Columns(cols, box_columns=bc))
+ self.attachmentw = urwid.Pile(lines)
+ return self.attachmentw
+
+ def _get_body_widget(self):
+ """creates/returns the widget that displays the mail body"""
+ if not self.bodyw:
+ cols = [MessageBodyWidget(self.message.get_email())]
+ bc = list()
+ if self.depth:
+ cols.insert(0, self._get_spacer(self.bars_at[1:]))
+ bc.append(0)
+ cols.insert(1, self._get_arrowhead_aligner())
+ bc.append(1)
+ self.bodyw = urwid.Columns(cols, box_columns=bc)
+ return self.bodyw
+
+ def _get_source_widget(self):
+ """creates/returns the widget that displays the mail body"""
+ if not self.sourcew:
+ cols = [urwid.Text(self.message.get_email().as_string())]
+ bc = list()
+ if self.depth:
+ cols.insert(0, self._get_spacer(self.bars_at[1:]))
+ bc.append(0)
+ cols.insert(1, self._get_arrowhead_aligner())
+ bc.append(1)
+ self.sourcew = urwid.Columns(cols, box_columns=bc)
+ return self.sourcew
+
+ def _get_spacer(self, bars_at):
+ prefixchars = []
+ length = len(bars_at)
+ for b in bars_at:
+ if b:
+ c = u'\u2502'
+ else:
+ c = ' '
+ 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):
+ if self.message.has_replies():
+ aligner = u'\u2502'
+ else:
+ aligner = ' '
+ aligner = urwid.SolidFill(aligner)
+ return ('fixed', 1, urwid.AttrMap(aligner, self.arrow_bars_att))
+
+ def selectable(self):
+ return True
+
+ def keypress(self, size, key):
+ return self.pile.keypress(size, key)
+
+ def get_message(self):
+ """get contained :class`~alot.db.message.Message`"""
+ return self.message
+
+ def get_email(self):
+ """get contained :class:`email <email.Message>`"""
+ return self.message.get_email()
+
+
+class MessageSummaryWidget(urwid.WidgetWrap):
+ """
+ one line summary of a :class:`~alot.db.message.Message`.
+ """
+
+ def __init__(self, message, even=True):
+ """
+ :param message: a message
+ :type message: alot.db.Message
+ :param even: even entry in a pile of messages? Used for theming.
+ :type even: bool
+ """
+ self.message = message
+ self.even = even
+ if even:
+ attr = settings.get_theming_attribute('thread', 'summary', 'even')
+ else:
+ attr = settings.get_theming_attribute('thread', 'summary', 'odd')
+ focus_att = settings.get_theming_attribute('thread', 'summary',
+ 'focus')
+ cols = []
+
+ sumstr = self.__str__()
+ txt = urwid.Text(sumstr)
+ cols.append(txt)
+
+ thread_tags = message.get_thread().get_tags(intersection=True)
+ outstanding_tags = set(message.get_tags()).difference(thread_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))
+ line = urwid.AttrMap(urwid.Columns(cols, dividechars=1), attr,
+ focus_att)
+ urwid.WidgetWrap.__init__(self, line)
+
+ def __str__(self):
+ author, address = self.message.get_author()
+ date = self.message.get_datestring()
+ rep = author if author != '' else address
+ if date is not None:
+ rep += " (%s)" % date
+ return rep
+
+ def selectable(self):
+ return True
+
+ def keypress(self, size, key):
+ return key
+
+
+class MessageBodyWidget(urwid.AttrMap):
+ """
+ displays printable parts of an email
+ """
+
+ def __init__(self, msg):
+ bodytxt = message.extract_body(msg)
+ att = settings.get_theming_attribute('thread', 'body')
+ urwid.AttrMap.__init__(self, urwid.Text(bodytxt), att)
diff --git a/alot/widgets/utils.py b/alot/widgets/utils.py
new file mode 100644
index 00000000..b50b2db9
--- /dev/null
+++ b/alot/widgets/utils.py
@@ -0,0 +1,64 @@
+# 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
+
+"""
+Utility Widgets not specific to alot
+"""
+import urwid
+import logging
+
+
+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)
+ self.title = urwid.Text(title)
+ if titleattr is not None:
+ self.title = urwid.AttrMap(self.title, titleattr)
+ if bodyattr is not None:
+ self.body = urwid.AttrMap(self.body, bodyattr)
+
+ box = urwid.Overlay(self.title, self.body,
+ align='center',
+ valign='top',
+ width=len(title),
+ height=None,
+ )
+ urwid.WidgetWrap.__init__(self, box)
+
+ def selectable(self):
+ return self.body.selectable()
+
+ def keypress(self, size, key):
+ return self.body.keypress(size, key)
+
+
+class CatchKeyWidgetWrap(urwid.WidgetWrap):
+ def __init__(self, widget, key, on_catch, relay_rest=True):
+ urwid.WidgetWrap.__init__(self, widget)
+ self.key = key
+ self.relay = relay_rest
+ self.on_catch = on_catch
+
+ def selectable(self):
+ return True
+
+ def keypress(self, size, key):
+ logging.debug('CATCH KEY: %s' % key)
+ logging.debug('relay: %s' % self.relay)
+ if key == self.key:
+ self.on_catch()
+ elif self._w.selectable() and self.relay:
+ return self._w.keypress(size, key)
diff --git a/docs/source/api/database.rst b/docs/source/api/database.rst
index 71fa50ef..ff66fd3e 100644
--- a/docs/source/api/database.rst
+++ b/docs/source/api/database.rst
@@ -5,23 +5,23 @@ Email Database
The python bindings to libnotmuch define :class:`notmuch.Thread` and
:class:`notmuch.Message`, which unfortunately are very fragile.
-Alot defines the wrapper classes :class:`Thread` and :class:`~alot.db.message.Message` that
-use an :class:`DBManager` instance to transparently provide persistent objects.
+Alot defines the wrapper classes :class:`alot.db.Thread` and :class:`alot.db.Message` that
+use an :class:`manager.DBManager` instance to transparently provide persistent objects.
-:class:`~alot.db.message.Message` moreover contains convenience methods
+:class:`alot.db.Message` moreover contains convenience methods
to extract information about the message like reformated header values, a summary,
decoded and interpreted body text and a list of :class:`Attachments <alot.db.attachment.Attachment>`.
-The central :class:`~alot.ui.UI` instance carries around a :class:`DBManager` object that
-is used for any lookups or modifications of the email base. :class:`DBManager` can
-directly look up :class:`Thread` and :class:`~alot.db.message.Message` objects and is able to
+The central :class:`~alot.ui.UI` instance carries around a :class:`~manager.DBManager` object that
+is used for any lookups or modifications of the email base. :class:`~manager.DBManager` can
+directly look up :class:`Thread` and :class:`~alot.db.Message` objects and is able to
postpone/cache/retry writing operations in case the Xapian index is locked by another
process.
Database Manager
-----------------
-.. autoclass:: DBManager
+.. autoclass:: alot.db.manager.DBManager
:members:
@@ -41,11 +41,11 @@ Errors
Wrapper
-------
-.. autoclass:: alot.db.thread.Thread
+.. autoclass:: alot.db.Thread
:members:
-.. autoclass:: alot.db.message.Message
+.. autoclass:: alot.db.Message
:members:
diff --git a/docs/source/api/interface.rst b/docs/source/api/interface.rst
index f9afb7ec..d2294e26 100644
--- a/docs/source/api/interface.rst
+++ b/docs/source/api/interface.rst
@@ -85,7 +85,29 @@ Widgets
What follows is a list of the non-standard urwid widgets used in alot.
Some of them respect :doc:`user settings <settings>`, themes in particular.
-.. automodule:: alot.widgets
+utils
+`````
+.. automodule:: alot.widgets.utils
+ :members:
+
+globals
+```````
+.. automodule:: alot.widgets.globals
+ :members:
+
+bufferlist
+``````````
+.. automodule:: alot.widgets.bufferlist
+ :members:
+
+search
+``````
+.. automodule:: alot.widgets.search
+ :members:
+
+thread
+``````
+.. automodule:: alot.widgets.thread
:members:
Completion
diff --git a/docs/source/api/settings.rst b/docs/source/api/settings.rst
index 2cb7ea74..4f8dc455 100644
--- a/docs/source/api/settings.rst
+++ b/docs/source/api/settings.rst
@@ -1,24 +1,29 @@
User Settings
=============
-.. module:: alot.settings
+.. module:: alot.settings.manager
+
+Alot sets up a :class:`SettingsManager` to access user settings
+defined in different places uniformly.
+There are four types of user settings:
+
++------------------------------------+----------------------------------+---------------------------------------------+
+| what? | location | accessible via |
++====================================+==================================+=============================================+
+| alot config | :file:`~/.config/alot/config` | :meth:`SettingsManager.get` |
+| | or given by command option `-c`. | |
++------------------------------------+----------------------------------+---------------------------------------------+
+| hooks -- user provided python code | :file:`~/.config/alot/hooks.py` | :meth:`SettingsManager.get_hook` |
+| | or as given by the `hooksfile` | |
+| | config value | |
++------------------------------------+----------------------------------+---------------------------------------------+
+| notmuch config | :file:`~/.notmuchrc` | :meth:`SettingsManager.get_notmuch_setting` |
+| | or given by command option `-n` | |
++------------------------------------+----------------------------------+---------------------------------------------+
+| mailcap -- defines shellcommands | :file:`~/.mailcap` | :meth:`SettingsManager.mailcap_find_match` |
+| to handle mime types | (:file:`/etc/mailcap`) | |
++------------------------------------+----------------------------------+---------------------------------------------+
-There are four types of user settings: notmuchs and alot's config
-files, the hooks-file for user provided python code and the mailcap,
-defining shell comands as handlers for files of certain mime types.
-Alot sets up :class:`SettingsManager` objects to access these user settings uniformly.
-
-MIME handlers can be looked up via :meth:`SettingsManager.get_mime_handler`,
-config values of alot and notmuch's config are accessible using
-:meth:`SettingsManager.get` and :meth:`SettingsManager.get_notmuch_setting`.
-These methods return either None or the requested value typed as indicated in
-the spec files :file:`alot/defaults/*spec`.
-
-Hooks can be looked up via :meth:`SettingsManager.get_hook`.
-They are user defined callables that expect to be called with the following parameters:
-
- :ui: :class:`~alot.ui.UI` -- the initialized main component
- :dbm: :class:`~alot.db.DBManager` -- :obj:`ui.dbman`
Settings Manager
----------------
diff --git a/docs/source/conf.py b/docs/source/conf.py
index a415a655..4dbe0b5b 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -19,8 +19,12 @@ class Mock(object):
def __getattr__(self, name):
return Mock() if name not in ('__file__', '__path__') else '/dev/null'
-MOCK_MODULES = ['notmuch', 'notmuch.globals',
- 'twisted', 'twisted.internet',
+class MockModule(object):
+ @classmethod
+ def __getattr__(self, name):
+ return Mock if name not in ('__file__', '__path__') else '/dev/null'
+
+MOCK_MODULES = ['twisted', 'twisted.internet',
'twisted.internet.defer',
'twisted.python',
'twisted.python.failure',
@@ -29,7 +33,10 @@ MOCK_MODULES = ['notmuch', 'notmuch.globals',
'magic',
'gpgme',
'argparse']
+MOCK_DIRTY = ['notmuch']
for mod_name in MOCK_MODULES:
+ sys.modules[mod_name] = MockModule()
+for mod_name in MOCK_DIRTY:
sys.modules[mod_name] = Mock()
# end of readthedocs.org hack
diff --git a/setup.py b/setup.py
index c754eeed..f5d5858e 100755
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ setup(name='alot',
url=alot.__url__,
license=alot.__copyright__,
packages=['alot', 'alot.commands', 'alot.settings', 'alot.db',
- 'alot.utils'],
+ 'alot.utils', 'alot.widgets'],
package_data={'alot': [
'defaults/alot.rc.spec',
'defaults/notmuch.rc.spec',