# Copyright (C) 2011-2012 Patrick Totzke # 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 logging import re import urwid from .globals import TagWidget from .globals import AttachmentWidget from ..settings.const import settings from ..db.utils import X_SIGNATURE_MESSAGE_HEADER from ..helper import string_sanitize class MessageSummaryWidget(urwid.WidgetWrap): """ one line summary of a :class:`~alot.db.message.Message`. """ def __init__(self, message, odd): """ :param message: a message :type message: alot.db.Message :param odd: odd entry in a pile of messages? Used for theming. :type odd: bool """ self.message = message self.odd = odd if odd: attr = settings.get_theming_attribute('thread', 'summary', 'odd') else: attr = settings.get_theming_attribute('thread', 'summary', 'even') focus_att = settings.get_theming_attribute('thread', 'summary', 'focus') cols = [] sumstr = self.__str__() txt = urwid.Text(sumstr) cols.append(txt) if settings.get('msg_summary_hides_threadwide_tags'): thread_tags = message.thread.get_tags(intersection=True) outstanding_tags = set(message.get_tags()).difference(thread_tags) tag_widgets = sorted(TagWidget(t, attr, focus_att) for t in outstanding_tags) else: tag_widgets = sorted(TagWidget(t, attr, focus_att) for t in message.get_tags()) 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) super().__init__(line) def __str__(self): mail = self.message.get_email() subj = mail.get_all('subject', ['']) subj = re.sub(r'\n\s+', r' ', ','.join(subj)) author, address = self.message.get_author() date = self.message.get_datestring() rep = '%s: %s' % (author if author != '' else address, subj) if date is not None: rep += " (%s)" % date return rep def selectable(self): return True def keypress(self, size, key): return key class FocusableText(urwid.WidgetWrap): """Selectable Text used for nodes in our example""" def __init__(self, txt, att, att_focus): t = urwid.Text(txt) w = urwid.AttrMap(t, att, att_focus) super().__init__(w) def selectable(self): return True def keypress(self, size, key): return key class HeadersWidget(urwid.WidgetWrap): """ A flow widget displaying message headers. """ _key_attr = None _value_attr = None _gaps_attr = None def __init__(self, key_attr, value_attr, gaps_attr): """ :param headerslist: list of key/value pairs to display :type headerslist: list of (str, str) :param key_attr: theming attribute to use for keys :type key_attr: urwid.AttrSpec :param value_attr: theming attribute to use for values :type value_attr: urwid.AttrSpec :param gaps_attr: theming attribute to wrap lines in :type gaps_attr: urwid.AttrSpec """ self._key_attr = key_attr self._value_attr = value_attr self._gaps_attr = gaps_attr super().__init__(urwid.Pile([])) def set_headers(self, headers): if len(headers) == 0: self._w.contents.clear() return max_key_len = max(map(lambda x: len(x[0]), headers)) widgets = [] for key, value in headers: # TODO even/odd keyw = ('fixed', max_key_len + 1, urwid.Text((self._key_attr, key))) valuew = urwid.Text((self._value_attr, value)) linew = urwid.AttrMap(urwid.Columns([keyw, valuew]), self._gaps_attr) widgets.append(linew) self._w.contents = [(w, ('pack', None)) for w in widgets] class MessageWidget(urwid.WidgetWrap): """ A flow widget displaying the contents of a single :class:`alot.db.Message`. Collapsing this message corresponds to showing the summary only. """ _display_all_headers = None _display_source = None _headers_wgt = None _source_wgt = None _body_wgt = None _attach_wgt = None _QUOTE_CHARS = '>|:}#' _QUOTE_REGEX = '(([ \t]*[{quote_chars}])+)'.format(quote_chars = _QUOTE_CHARS) def __init__(self, message): """ :param message: Message to display :type message: alot.db.Message """ self._message = message self._headers_wgt = self._get_headers() self._source_wgt = self._get_source() self._body_wgt = self._get_body() self._attach_wgt = self._get_attachments() super().__init__(urwid.ListBox(urwid.SimpleListWalker([]))) self.display_all_headers = False self.display_source = False def get_message(self): return self._message def _reassemble(self, display_source): widgets = [] if display_source: widgets.append(self._source_wgt) else: widgets.append(self._headers_wgt) if self._attach_wgt is not None: widgets.append(self._attach_wgt) widgets.append(self._body_wgt) self._w.body[:] = widgets def _get_source(self): sourcetxt = self._message.get_email().as_string() sourcetxt = string_sanitize(sourcetxt) att = settings.get_theming_attribute('thread', 'body') att_focus = settings.get_theming_attribute('thread', 'body_focus') return FocusableText(sourcetxt, att, att_focus) def _get_body(self): attr_body = settings.get_theming_attribute('thread', 'body') attrs_quote = settings.get_quote_theming() body_lines = self._message.get_body_text().splitlines() quote_levels = [] for line in body_lines: level = 0 m = re.match(self._QUOTE_REGEX, line) if m is not None: g = m.group(0) for c in self._QUOTE_CHARS: level += g.count(c) quote_levels.append(level) line_widgets = [] for level, line in zip(quote_levels, body_lines): if level == 0 or len(attrs_quote) < 1: attr = attr_body else: attr = attrs_quote[(level - 1) % len(attrs_quote)] line_widgets.append(urwid.Text((attr, line))) return urwid.Pile(line_widgets) def _get_headers(self): key_attr = settings.get_theming_attribute('thread', 'header_key') value_attr = settings.get_theming_attribute('thread', 'header_value') gaps_attr = settings.get_theming_attribute('thread', 'header') return HeadersWidget(key_attr, value_attr, gaps_attr) def _get_attachments(self): alist = [] for a in self._message.get_attachments(): alist.append(AttachmentWidget(a)) if alist: return urwid.Pile(alist) return None @property def display_all_headers(self): return self._display_all_headers @display_all_headers.setter def display_all_headers(self, val): val = bool(val) if val == self._display_all_headers: return mail = self._message.get_email() lines = [] if val: # collect all header/value pairs in the order they appear lines = list(mail.items()) else: # only a selection of headers should be displayed. # use order of the `headers` parameter for key in settings.get('displayed_headers'): if key in mail: if key.lower() in ['cc', 'bcc', 'to']: lines.append((key, ', '.join(mail.get_all(key)))) else: for value in mail.get_all(key): lines.append((key, value)) elif key.lower() == 'tags': # TODO this should be in a separate widget logging.debug('want tags header') values = [] for t in self._message.get_tags(): tagrep = settings.get_tagstring_representation(t) if t is not tagrep['translated']: t = '%s (%s)' % (tagrep['translated'], t) values.append(t) lines.append((key, ', '.join(values))) # OpenPGP pseudo headers # TODO this should be in a separate widget if mail[X_SIGNATURE_MESSAGE_HEADER]: lines.append(('PGP-Signature', mail[X_SIGNATURE_MESSAGE_HEADER])) self._headers_wgt.set_headers(lines) self._display_all_headers = val @property def display_source(self): return self._display_source @display_source.setter def display_source(self, val): val = bool(val) if val == self._display_source: return self._reassemble(val) self._display_source = val def get_selected_attachment(self): """ If an AttachmentWidget is currently focused, return it. Otherwise return None. """ if self._w.focus is self._attach_wgt: return self._attach_wgt.focus.attachment return None class ThreadNode(urwid.WidgetWrap): _ARROW = '➤' _HBAR = '─' _VBAR = '│' _TNODE = '├' _CORNER = '└' _decor_text = None _have_next_sibling = None def __init__(self, msg, thread, pos, indent): msg_summary = MessageSummaryWidget(msg, pos & 1) if msg.depth == 0: wgt = msg_summary else: self._decor_text = urwid.Text('') wgt = urwid.Columns([('pack', self._decor_text), msg_summary]) ancestor_chain = [msg] for p in msg.parents(): ancestor_chain.insert(0, p) have_sibling = [] for d, m in enumerate(ancestor_chain): if d == 0: have_sibling.append(m != thread.toplevel_messages[-1]) else: have_sibling.append(m != ancestor_chain[d - 1].replies[-1]) self._have_next_sibling = have_sibling super().__init__(wgt) self.set_indent(indent) def set_indent(self, indent): if self._decor_text is None: return seg_text = [] if indent > 0: depth = len(self._have_next_sibling) - 1 for d in range(depth): if d == depth - 1: corner = self._TNODE if self._have_next_sibling[d + 1] else self._CORNER seg_text.append(corner + self._ARROW) else: bar = self._VBAR if self._have_next_sibling[d + 1] else ' ' seg_text.append(bar + ' ') self._decor_text.set_text(''.join(seg_text))