diff --git a/alot/__main__.py b/alot/__main__.py index 6202daf1e..43b2ea68a 100644 --- a/alot/__main__.py +++ b/alot/__main__.py @@ -96,8 +96,13 @@ def debuglogstring(val): ] search_help = "start in a search buffer using the querystring provided "\ "as parameter. See the SEARCH SYNTAX section of notmuch(1)." + searchmessages_help = "start in a search messages buffer using the "\ + "querystring provided as parameter. See the SEARCH"\ + " SYNTAX section of notmuch(1)." subCommands = [['search', None, SearchOptions, search_help], + ['searchmessages', None, SearchOptions, + searchmessages_help], ['compose', None, ComposeOptions, "compose a new message"]] def opt_version(self): @@ -173,6 +178,10 @@ def main(): query = ' '.join(args.subOptions.args) cmdstring = 'search %s %s' % (args.subOptions.as_argparse_opts(), query) + elif args.subCommand == 'searchmessages': + query = ' '.join(args.subOptions.args) + cmdstring = 'searchmessages %s %s' % ( + args.subOptions.as_argparse_opts(), query) elif args.subCommand == 'compose': cmdstring = 'compose %s' % args.subOptions.as_argparse_opts() if args.subOptions.rest is not None: diff --git a/alot/buffers.py b/alot/buffers.py index 4cb3d50a6..bc88a4efe 100644 --- a/alot/buffers.py +++ b/alot/buffers.py @@ -12,12 +12,14 @@ 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.globals import ( + TagWidget, AlwaysFocused, HeadersList, AttachmentWidget, + ModifiedOnFocusChangeWalker) from alot.widgets.bufferlist import BufferlineWidget -from alot.widgets.search import ThreadlineWidget +from alot.widgets.search import ( + ThreadlineWidget, SearchMessageSummaryWidget) from alot.widgets.thread import ThreadTree +from alot.widgets.message import MessageViewer from urwidtrees import ArrowTree, TreeBox, NestedTree @@ -672,3 +674,229 @@ def get_selected_tag(self): (cols, pos) = self.taglist.get_focus() tagwidget = cols.original_widget.get_focus() return tagwidget.get_tag() + + +class SearchMessagesBuffer(SearchBuffer): + """ + Shows a result list of message for a query, or messages for + a thread. + """ + + modename = 'searchmessages' + + def __init__(self, ui, initialquery='', thread=None, sort_order=None, + focus_oldest_with_tags=None): + """ + May receive a query string or a thread object. If a thread object is + passed, all the messages from it are displayed instead of using the + query string. + """ + + # cache for already rendered messages + self.message_viewers = {} + + # two semaphores for auto-removal of unread tag + self._auto_unread_dont_touch_mids = set([]) + self._auto_unread_writing = False + + self.thread = thread + + SearchBuffer.__init__(self, ui, initialquery, sort_order) + + if focus_oldest_with_tags: + self.set_focus_by_tags(focus_oldest_with_tags) + + def __str__(self): + content = self.querystring if self.querystring else self.thread + formatstring = '[searchmessages] for "%s" (%d message%s)' + return formatstring % (content, self.result_count, + 's' * (not (self.result_count == 1))) + + def get_info(self): + info = {} + info['querystring'] = self.querystring + info['subject'] = self.thread.get_subject() if self.thread else '' + info['result_count'] = self.result_count + info['result_count_positive'] = 's' * (not (self.result_count == 1)) + return info + + def rebuild(self, reverse=False): + self.isinitialized = True + self.reversed = reverse + self.kill_filler_process() + + # if this buffer received a thread object directly + if self.thread: + messages = [SearchMessageSummaryWidget(message=m) + for m in sorted(self.thread.get_messages(), + key=lambda m: m.get_date())] + # if should display newest messages first + if self.sort_order == 'newest_first' or reverse: + messages = list(reversed(messages)) + self.result_count = len(messages) + self.messagelist = ModifiedOnFocusChangeWalker(messages) + # if no thread object was passed and a query is needed + else: + self.result_count = self.dbman.count_messages(self.querystring) + if reverse: + order = self._REVERSE[self.sort_order] + else: + order = self.sort_order + + try: + self.pipe, self.proc = self.dbman.get_messages( + self.querystring, order) + except NotmuchError: + self.ui.notify( + 'malformed query string: %s' % self.querystring, 'error') + self.listbox = urwid.ListBox([]) + self.body = self.listbox + return + + self.messagelist = PipeWalker( + self.pipe, SearchMessageSummaryWidget, + dbman=self.dbman, reverse=reverse) + + self.listbox = urwid.ListBox(self.messagelist) + + self.message_viewer = self.get_message_viewer( + self.get_selected_message()) + + self.body = urwid.Frame( + self.message_viewer, + urwid.Pile([ + AlwaysFocused(urwid.BoxAdapter(self.listbox, 5)), + (1, urwid.Filler( + urwid.AttrMap(urwid.Divider(u"_"), 'bright'))), + ]) + ) + + def set_focus_by_tags(self, tags, oldest=True): + ''' + Set focus to the last or oldest message that contains 'tags'. + ''' + tags = set(tags) + oldest_date = None + focus = self.listbox.get_focus()[1] + for i, line in enumerate(self.listbox.body): + msg = line.message + if tags.issubset(msg.get_tags()): + if oldest: + date = msg.get_date() + if oldest_date is None or date < oldest_date: + oldest_date = date + focus = i + else: + focus = i + self.listbox.set_focus(focus) + self.possible_message_focus_change() + + def keypress(self, size, key): + if key == 'next': + return self.listbox.keypress(size, 'down') + elif key == 'previous': + return self.listbox.keypress(size, 'up') + else: + return self.body.keypress(size, key) + + def consume_pipe(self): + while not self.messagelist.empty: + self.messagelist._get_next_item() + + def focus_first(self): + if not self.reversed: + self.listbox.set_focus(0) + else: + self.rebuild(reverse=False) + self.possible_message_focus_change() + + def focus_last(self): + if self.reversed: + self.listbox.set_focus(0) + elif (self.result_count < 200) or \ + (self.sort_order not in self._REVERSE.keys()): + if self.thread: + num_lines = self.result_count + else: + self.consume_pipe() + num_lines = len(self.messagelist.get_lines()) + self.listbox.set_focus(num_lines - 1) + else: + self.rebuild(reverse=True) + self.possible_message_focus_change() + + def get_selected_messageline(self): + """ + returns curently focussed :class:`alot.widgets.MessagelineWidget` + from the result list. + """ + (messagelinewidget, size) = self.messagelist.get_focus() + return messagelinewidget + + def get_selected_message(self): + """returns currently selected :class:`~alot.db.Message`""" + messagelinewidget = self.get_selected_messageline() + message = None + if messagelinewidget: + message = messagelinewidget.get_message() + return message + + def get_selected_mid(self): + return self.get_selected_message().get_message_id() + + def get_message_viewer(self, message=None): + """Returns a message viewer for arg message, storing it in cache.""" + if not message: + message = self.get_selected_message() + mid = message.get_message_id() + mv = self.message_viewers.get(mid) + if not mv: + mv = MessageViewer(message) + self.message_viewers[mid] = mv + return mv + + def possible_message_focus_change(self): + message = self.get_selected_message() + if self.message_viewer.get_message() != message: + mv = self.get_message_viewer(message) + self.body.contents['body'] = (mv, None) + self.message_viewer = mv + + def render(self, size, focus=False): + if settings.get('auto_remove_unread'): + logging.debug('SMbuffer: auto remove unread tag from msg?') + msg = self.get_selected_message() + ml = self.get_selected_messageline() + mid = msg.get_message_id() + if mid not in self._auto_unread_dont_touch_mids: + if 'unread' in msg.get_tags(): + logging.debug('SMbuffer: removing unread') + + def clear(): + self._auto_unread_writing = False + ml.refresh() + + self._auto_unread_dont_touch_mids.add(mid) + self._auto_unread_writing = True + msg.remove_tags(['unread'], afterwards=clear) + fcmd = commands.globals.FlushCommand(silent=True) + self.ui.apply_command(fcmd) + else: + logging.debug('SMbuffer: No, msg not unread') + else: + logging.debug('SMbuffer: No, mid locked for autorm-unread') + return self.body.render(size, focus) + + def message_trees(self): + # TODO: weird method name, but used to implement TagCommand + return self.messagelist + + def get_selected_messagetree(self): + # TODO: weird method name, but used to implement TagCommand + return self.get_selected_messageline() + + def refresh(self): + self.get_selected_messageline().refresh() + + def get_focus(self): + return self.body.get_body() diff --git a/alot/commands/__init__.py b/alot/commands/__init__.py index 095c068d6..908b7dff8 100644 --- a/alot/commands/__init__.py +++ b/alot/commands/__init__.py @@ -35,6 +35,7 @@ class CommandCanceled(Exception): COMMANDS = { 'search': {}, + 'searchmessages': {}, 'envelope': {}, 'bufferlist': {}, 'taglist': {}, diff --git a/alot/commands/globals.py b/alot/commands/globals.py index d234f54fc..8861af6da 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -102,6 +102,37 @@ def apply(self, ui): ui.notify('empty query string') +@registerCommand(MODE, 'searchmessages', usage='search query', arguments=[ + (['--sort'], {'help': 'sort order', 'choices': [ + 'oldest_first', 'newest_first', 'message_id', 'unsorted']}), + (['query'], {'nargs': argparse.REMAINDER, + 'help': 'searchmessages string'})]) +class SearchMessagesCommand(SearchCommand): + + """open a new search messages buffer""" + + def apply(self, ui): + if self.query: + open_searches = ui.get_buffers_of_type( + buffers.SearchMessagesBuffer) + to_be_focused = None + for sb in open_searches: + if sb.querystring == self.query: + to_be_focused = sb + if to_be_focused: + if ui.current_buffer != to_be_focused: + ui.buffer_focus(to_be_focused) + else: + # refresh an already displayed search + ui.current_buffer.rebuild() + ui.update() + else: + ui.buffer_open(buffers.SearchMessagesBuffer( + ui, self.query, sort_order=self.order)) + else: + ui.notify('empty query string') + + @registerCommand(MODE, 'prompt', arguments=[ (['startwith'], {'nargs': '?', 'default': '', 'help': 'initial content'})]) class PromptCommand(Command): diff --git a/alot/commands/search.py b/alot/commands/search.py index 0001a0bdb..73ab99d2a 100644 --- a/alot/commands/search.py +++ b/alot/commands/search.py @@ -40,6 +40,29 @@ def apply(self, ui): sb.unfold_matching(query) +@registerCommand(MODE, 'selectthreadmessages') +class OpenThreadMessagesCommand(Command): + + """open thread in a new buffer""" + def __init__(self, thread=None, **kwargs): + """ + :param thread: thread to open (Uses focussed thread if unset) + :type thread: :class:`~alot.db.Thread` + """ + self.thread = thread + Command.__init__(self, **kwargs) + + def apply(self, ui): + if not self.thread: + self.thread = ui.current_buffer.get_selected_thread() + # TODO: allow custom focus_oldest_with_tags + ui.buffer_open( + buffers.SearchMessagesBuffer( + ui, thread=self.thread, sort_order=None, + focus_oldest_with_tags=['inbox', 'unread'] + )) + + @registerCommand(MODE, 'refine', help='refine query', arguments=[ (['--sort'], {'help': 'sort order', 'choices': [ 'oldest_first', 'newest_first', 'message_id', 'unsorted']}), diff --git a/alot/commands/searchmessages.py b/alot/commands/searchmessages.py new file mode 100644 index 000000000..a5e8191db --- /dev/null +++ b/alot/commands/searchmessages.py @@ -0,0 +1,166 @@ +# 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 +import argparse +import logging + +from alot.commands import Command, registerCommand +from alot.commands.globals import MoveCommand +from alot.commands.thread import ( + ReplyCommand, ForwardCommand, EditNewCommand, PipeCommand, TagCommand, + SaveAttachmentCommand, OpenAttachmentCommand) +from alot.utils.booleanaction import BooleanAction + +from alot.widgets.globals import AttachmentWidget + + +MODE = 'searchmessages' + + +@registerCommand(MODE, 'move', help='move focus in search buffer', + arguments=[(['movement'], { + 'nargs': argparse.REMAINDER, + 'help': 'last'})]) +class MoveFocusCommand(MoveCommand): + + def apply(self, ui): + logging.debug(self.movement) + tbuffer = ui.current_buffer + if self.movement == 'last': + tbuffer.focus_last() + ui.update() + elif self.movement == 'next' or self.movement == 'previous': + ui.mainloop.process_input([self.movement]) + tbuffer.possible_message_focus_change() + # tbuffer.focus_next() + else: + MoveCommand.apply(self, ui) + + +@registerCommand(MODE, 'reply', arguments=[ + (['--all'], {'action': 'store_true', 'help': 'reply to all'}), + (['--list'], {'action': BooleanAction, 'default': None, + 'dest': 'listreply', 'help': 'reply to list'}), + (['--spawn'], {'action': BooleanAction, 'default': None, + 'help': 'open editor in new window'})]) +class SMReplyCommand(ReplyCommand): + pass + + +@registerCommand(MODE, 'forward', arguments=[ + (['--attach'], {'action': 'store_true', 'help': 'attach original mail'}), + (['--spawn'], {'action': BooleanAction, 'default': None, + 'help': 'open editor in new window'})]) +class SMForwardCommand(ForwardCommand): + pass + + +# TODO: getting error when trying to edit a message made by someone +# that is not me, because cant find account settings. +# Does it happens on master? +@registerCommand(MODE, 'editnew', arguments=[ + (['--spawn'], {'action': BooleanAction, 'default': None, + 'help': 'open editor in new window'})]) +class SMEditNewCommand(EditNewCommand): + pass + + +@registerCommand(MODE, 'pipeto', arguments=[ + (['cmd'], {'help': 'shellcommand to pipe to', 'nargs': '+'}), + (['--all'], {'action': 'store_true', 'help': 'pass all messages'}), + (['--format'], {'help': 'output format', 'default': 'raw', + 'choices': ['raw', 'decoded', 'id', 'filepath']}), + (['--separately'], {'action': 'store_true', + 'help': 'call command once for each message'}), + (['--background'], {'action': 'store_true', + 'help': 'don\'t stop the interface'}), + (['--add_tags'], {'action': 'store_true', + 'help': 'add \'Tags\' header to the message'}), + (['--shell'], {'action': 'store_true', + 'help': 'let the shell interpret the command'}), + (['--notify_stdout'], {'action': 'store_true', + 'help': 'display cmd\'s stdout as notification'}), +], +) +class SMPipeCommand(PipeCommand): + pass + + +@registerCommand(MODE, 'toggletags', forced={'action': 'toggle'}, arguments=[ + (['--all'], {'action': 'store_true', + 'help': 'tag all messages in thread'}), + (['--no-flush'], {'action': 'store_false', 'dest': 'flush', + 'help': 'postpone a writeout to the index'}), + (['tags'], {'help': 'comma separated list of tags'})], + help='flip presence of tags on message(s)', +) +class SMTagCommand(TagCommand): + pass + + +@registerCommand(MODE, 'save', arguments=[ + (['--all'], {'action': 'store_true', 'help': 'save all attachments'}), + (['path'], {'nargs': '?', 'help': 'path to save to'})]) +class SMSaveAttachmentCommand(SaveAttachmentCommand): + pass + + +@registerCommand(MODE, 'select') +class SelectCommand(Command): + + """select focussed element. The fired action depends on the focus: + - if message summary, this toggles visibility of the message, + - if attachment line, this opens the attachment""" + def apply(self, ui): + focus = ui.get_deep_focus() + if isinstance(focus, AttachmentWidget): + logging.info('open attachment') + ui.apply_command(OpenAttachmentCommand(focus.get_attachment())) + + +@registerCommand(MODE, 'togglesource', forced={'raw': 'toggle'}, arguments=[ + (['query'], {'help': 'query used to filter messages to affect', + 'nargs': '*'}), +], help='display message source') +@registerCommand(MODE, 'toggleheaders', forced={'all_headers': 'toggle'}, + arguments=[ + (['query'], { + 'help': 'query used to filter messages to affect', + 'nargs': '*'}), + ], + help='display all headers') +class ChangeDisplaymodeCommand(Command): + + """toggle source or headers""" + repeatable = True + + def __init__(self, query=None, visible=None, raw=None, all_headers=None, + **kwargs): + """ + :param query: notmuch query string used to filter messages to affect + :type query: str + :param visible: unfold if `True`, fold if `False`, ignore if `None` + :type visible: True, False, 'toggle' or None + :param raw: display raw message text. + :type raw: True, False, 'toggle' or None + :param all_headers: show all headers (only visible if not in raw mode) + :type all_headers: True, False, 'toggle' or None + """ + self.raw = raw + self.all_headers = all_headers + Command.__init__(self, **kwargs) + + def apply(self, ui): + mv = ui.current_buffer.get_message_viewer() + raw = not mv.display_source if self.raw == 'toggle' else self.raw + all_headers = not mv.display_all_headers \ + if self.all_headers == 'toggle' else self.all_headers + + if raw is not None: + mv.display_source = raw + if all_headers is not None: + mv.display_all_headers = all_headers + mv.refresh() + +# TODO: implement bounce, print, refine +# TODO: maybe also implement refineprompt, retagprompt, remove? diff --git a/alot/db/manager.py b/alot/db/manager.py index be80b4dca..a5c263b61 100644 --- a/alot/db/manager.py +++ b/alot/db/manager.py @@ -367,6 +367,25 @@ def threaded_reader(prefix, fd): sender.close() return receiver, process + def get_messages(self, querystring, sort='newest_first'): + """ + asynchronously look up messages ids matching `querystring`. + + :param querystring: The query string to use for the lookup + :type querystring: str. + :param sort: Sort order. one of ['oldest_first', 'newest_first', + 'message_id', 'unsorted'] + :type query: str + :returns: a pipe together with the process that asynchronously + writes to it. + :rtype: (:class:`multiprocessing.Pipe`, + :class:`multiprocessing.Process`) + """ + assert sort in self._sort_orders.keys() + q = self.query(querystring) + q.set_sort(self._sort_orders[sort]) + return self.async(q.search_messages, (lambda a: a.get_message_id())) + def get_threads(self, querystring, sort='newest_first'): """ asynchronously look up thread ids matching `querystring`. diff --git a/alot/defaults/alot.rc.spec b/alot/defaults/alot.rc.spec index a02fecd51..71b12140c 100644 --- a/alot/defaults/alot.rc.spec +++ b/alot/defaults/alot.rc.spec @@ -116,6 +116,8 @@ bufferlist_statusbar = mixed_list(string, string, default=list('[{buffer_no}: bu # * `{result_count_positive}`: 's' if result count is greater than 0. search_statusbar = mixed_list(string, string, default=list('[{buffer_no}: search] for "{querystring}"','{input_queue} {result_count} of {total_messages} messages')) +searchmessages_statusbar = mixed_list(string, string, default=list('[{buffer_no}: messages] for "{querystring}" {subject}','{input_queue} {result_count} of {total_messages} messages')) + # Format of the status-bar in thread mode. # This is a pair of strings to be left and right aligned in the status-bar. # Apart from the global variables listed at :ref:`bufferlist_statusbar ` diff --git a/alot/widgets/globals.py b/alot/widgets/globals.py index 50fc032fe..2170bbc2a 100644 --- a/alot/widgets/globals.py +++ b/alot/widgets/globals.py @@ -310,3 +310,22 @@ def set_focussed(self): def set_unfocussed(self): self.set_attr_map(self.attmap['normal']) + + +class AlwaysFocused(urwid.WidgetWrap): + """ + Makes the wrapped widget render always as focused. + """ + + def __init__(self, w): + urwid.WidgetWrap.__init__(self, w) + + def render(self, size, focus): + return self._w.render(size, True) + + +class ModifiedOnFocusChangeWalker(urwid.SimpleFocusListWalker): + + def set_focus(self, focus): + self.focus = focus + self._modified() diff --git a/alot/widgets/message.py b/alot/widgets/message.py new file mode 100644 index 000000000..f2f15f6b6 --- /dev/null +++ b/alot/widgets/message.py @@ -0,0 +1,199 @@ +# 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 message viewer +""" +import urwid +import logging + +from alot.settings import settings +from alot.db.utils import decode_header, X_SIGNATURE_MESSAGE_HEADER +from alot.widgets.globals import AttachmentWidget +from alot.db.utils import extract_body + + +class SimpleDictList(urwid.Pile): + """ + :class:`SimpleTree` that displays key-value pairs. + + The structure will obey the Tree API but will not actually be a tree + but a flat list: It contains one top-level node (displaying the k/v pair in + Columns) per pair. That is, the root will be the first pair, + its sibblings will be the other pairs and first|last_child will always + be None. + """ + def __init__(self, content, key_attr, value_attr, gaps_attr=None): + """ + :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 + """ + max_key_len = 1 + structure = [] + # calc max length of key-string + for key, value in content: + if len(key) > max_key_len: + max_key_len = len(key) + for key, value in content: + # todo : even/odd + keyw = ('fixed', max_key_len + 1, + urwid.Text((key_attr, key))) + valuew = urwid.Text((value_attr, value)) + line = urwid.Columns([keyw, valuew]) + if gaps_attr is not None: + line = urwid.AttrMap(line, gaps_attr) + structure.append(line) + urwid.Pile.__init__(self, structure) + + +class MessageViewer(urwid.ListBox): + """ + :class:`Message` that displays contents of a single + :class:`alot.db.Message`. + """ + def __init__(self, message, odd=True): + """ + :param message: Message to display + :type message: alot.db.Message + :param odd: theme summary widget as if this is an odd line + (in the message-pile) + :type odd: bool + """ + self._message = message + self._odd = odd + self.display_source = False + self._bodytree = None + self._sourcetree = None + self.display_all_headers = False + self._all_headers_tree = None + self._default_headers_tree = None + self.display_attachments = True + self._attachments = None + + self._contentlist = urwid.SimpleListWalker(self._assemble_structure()) + urwid.ListBox.__init__(self, self._contentlist) + + def get_message(self): + return self._message + + def refresh(self): + self._summaryw = None + self._contentlist[:] = self._assemble_structure() + + def debug(self): + logging.debug('display_source %s' % self.display_source) + logging.debug('display_all_headers %s' % self.display_all_headers) + logging.debug('display_attachements %s' % self.display_attachments) + logging.debug('AHT %s' % str(self._all_headers_tree)) + logging.debug('DHT %s' % str(self._default_headers_tree)) + + def _assemble_structure(self): + mainstruct = [] + if self.display_source: + mainstruct.append(self._get_source()) + else: + mainstruct.append(self._get_headers()) + + attachmenttree = self._get_attachments() + if attachmenttree is not None: + mainstruct.append(attachmenttree) + + bodytree = self._get_body() + if bodytree is not None: + mainstruct.append(self._get_body()) + + return [urwid.Pile(mainstruct)] + + def _get_source(self): + if self._sourcetree is None: + sourcetxt = self._message.get_email().as_string() + # TODO: use theming? + # att = settings.get_theming_attribute('thread', 'body') + # att_focus = settings.get_theming_attribute('thread', + # 'body_focus') + # self._sourcetree = urwid.Text(sourcetxt, att, att_focus) + self._sourcetree = urwid.Text(sourcetxt) + return self._sourcetree + + def _get_body(self): + if self._bodytree is None: + bodytxt = extract_body(self._message.get_email()) + if bodytxt: + # TODO: use theming? + # att = settings.get_theming_attribute('thread', 'body') + # att_focus = settings.get_theming_attribute( + # 'thread', 'body_focus') + # self._bodytree = MessageText(bodytxt, att, att_focus) + self._bodytree = urwid.Text(bodytxt) + return self._bodytree + + def _get_headers(self): + if self.display_all_headers is True: + if self._all_headers_tree is None: + self._all_headers_tree = self.construct_header_pile() + ret = self._all_headers_tree + else: + if self._default_headers_tree is None: + headers = settings.get('displayed_headers') + self._default_headers_tree = self.construct_header_pile( + headers) + ret = self._default_headers_tree + return ret + + def _get_attachments(self): + if self._attachments is None: + alist = [] + for a in self._message.get_attachments(): + alist.append(AttachmentWidget(a)) + if alist: + self._attachments = urwid.Pile(alist) + return self._attachments + + def construct_header_pile(self, headers=None, normalize=True): + mail = self._message.get_email() + lines = [] + + if headers is None: + # collect all header/value pairs in the order they appear + headers = mail.keys() + for key, value in mail.items(): + dvalue = decode_header(value, normalize=normalize) + lines.append((key, dvalue)) + else: + # only a selection of headers should be displayed. + # use order of the `headers` parameter + for key in headers: + if key in mail: + if key.lower() in ['cc', 'bcc', 'to']: + values = mail.get_all(key) + values = [decode_header( + v, normalize=normalize) for v in values] + lines.append((key, ', '.join(values))) + else: + for value in mail.get_all(key): + dvalue = decode_header(value, normalize=normalize) + lines.append((key, dvalue)) + elif key.lower() == 'tags': + 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 + if mail[X_SIGNATURE_MESSAGE_HEADER]: + lines.append(('PGP-Signature', mail[X_SIGNATURE_MESSAGE_HEADER])) + + key_att = settings.get_theming_attribute('thread', 'header_key') + value_att = settings.get_theming_attribute('thread', 'header_value') + gaps_att = settings.get_theming_attribute('thread', 'header') + return SimpleDictList(lines, key_att, value_att, gaps_att) diff --git a/alot/widgets/search.py b/alot/widgets/search.py index 70c0064a6..0ce70edfd 100644 --- a/alot/widgets/search.py +++ b/alot/widgets/search.py @@ -11,6 +11,7 @@ from alot.helper import tag_cmp from alot.widgets.utils import AttrFlipWidget from alot.widgets.globals import TagWidget +from alot.widgets.thread import MessageSummaryWidget class ThreadlineWidget(urwid.AttrMap): @@ -184,3 +185,23 @@ def _get_theme(self, component, focus=False): else: path.append('normal') return settings.get_theming_attribute(path) + + +class SearchMessageSummaryWidget(MessageSummaryWidget): + + def __init__(self, mid=None, dbman=None, message=None): + """ + May receive a message id and db object, or directly a message object. + """ + if message: + self.message = message + else: + self.message = dbman.get_message(mid) + # TODO: handle even/odd theming passing arg below + MessageSummaryWidget.__init__(self, self.message, False) + + def get_message(self): + return self.message + + def refresh(self): + self._w = self._build_line() diff --git a/alot/widgets/thread.py b/alot/widgets/thread.py index e0d739b85..331fdc4cc 100644 --- a/alot/widgets/thread.py +++ b/alot/widgets/thread.py @@ -30,7 +30,10 @@ def __init__(self, message, even=True): """ self.message = message self.even = even - if even: + urwid.WidgetWrap.__init__(self, self._build_line()) + + def _build_line(self): + if self.even: attr = settings.get_theming_attribute('thread', 'summary', 'even') else: attr = settings.get_theming_attribute('thread', 'summary', 'odd') @@ -43,20 +46,21 @@ def __init__(self, message, even=True): cols.append(txt) if settings.get('msg_summary_hides_threadwide_tags'): - thread_tags = message.get_thread().get_tags(intersection=True) - outstanding_tags = set(message.get_tags()).difference(thread_tags) + thread_tags = self.message.get_thread().get_tags(intersection=True) + outstanding_tags = set( + self.message.get_tags()).difference(thread_tags) tag_widgets = [TagWidget(t, attr, focus_att) for t in outstanding_tags] else: tag_widgets = [TagWidget(t, attr, focus_att) - for t in message.get_tags()] + for t in self.message.get_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) + return line def __str__(self): author, address = self.message.get_author()