diff options
-rw-r--r-- | alot/account.py | 2 | ||||
-rw-r--r-- | alot/buffers.py | 24 | ||||
-rw-r--r-- | alot/commands/__init__.py | 4 | ||||
-rw-r--r-- | alot/commands/envelope.py | 20 | ||||
-rw-r--r-- | alot/commands/globals.py | 39 | ||||
-rw-r--r-- | alot/commands/search.py | 6 | ||||
-rw-r--r-- | alot/commands/thread.py | 31 | ||||
-rw-r--r-- | alot/ui.py | 20 | ||||
-rw-r--r-- | alot/widgets.py | 738 | ||||
-rw-r--r-- | alot/widgets/__init__.py | 0 | ||||
-rw-r--r-- | alot/widgets/bufferlist.py | 29 | ||||
-rw-r--r-- | alot/widgets/globals.py | 203 | ||||
-rw-r--r-- | alot/widgets/search.py | 186 | ||||
-rw-r--r-- | alot/widgets/thread.py | 301 | ||||
-rw-r--r-- | alot/widgets/utils.py | 64 | ||||
-rwxr-xr-x | setup.py | 2 |
16 files changed, 859 insertions, 810 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: @@ -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) @@ -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', |