summaryrefslogtreecommitdiff
path: root/alot
diff options
context:
space:
mode:
authorPatrick Totzke <patricktotzke@gmail.com>2012-08-05 12:28:34 +0100
committerPatrick Totzke <patricktotzke@gmail.com>2012-08-10 13:03:39 +0100
commit14178c4e59850d4340e116ce29764cbd469c33be (patch)
tree3e0e47d32c88aeabf17b3250ef2aa5fe7c54fbbe /alot
parent6c80ee5ecd23ec54604eab3ada0e2573e1757d89 (diff)
cleanup: split widgets.py and pep8/pyflakes fixes
Diffstat (limited to 'alot')
-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/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
15 files changed, 858 insertions, 809 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/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..96fec312
--- /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 :ref:`urwid.Widgets` 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)