# Copyright (C) 2011-2018 Patrick Totzke # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file import asyncio from functools import partial import urwid from .buffer import Buffer from ..db.errors import QueryError from ..db.sort import NAME as SORT_NAME from ..settings.const import settings from ..widgets.search import ThreadlineWidget class IterWalker(urwid.ListWalker): """ urwid.ListWalker that reads next items from a generator and wraps them in ThreadlineWidget widgets for displaying """ _threads = None _wgt_factory = None _reverse = None _iter_done = False # list of the thread IDs _tids = None # a dictionary of threadline widgets, indexed by position _wgts = None _focus = None def __init__(self, threads, wgt_factory, reverse): self._threads = threads self._wgt_factory = wgt_factory self._reverse = reverse self._tids = [] self._wgts = {} self._focus = 0 super().__init__() def __len__(self): while not self._iter_done: self._get_next_item() return len(self._tids) def _map_pos(self, pos): if self._reverse: pos = len(self) - 1 - pos return pos def __getitem__(self, pos): self._check_pos(pos) if not pos in self._wgts: # make sure an exception while constructing the widget does not get # swallowed by urwid try: self._wgts[pos] = self._wgt_factory(self._tids[self._map_pos(pos)]) except (KeyError, IndexError, TypeError) as e: raise ValueError('Exception while constructing threadline widget') from e return self._wgts[pos] def _check_pos(self, pos): tgt_pos = self._map_pos(pos) if tgt_pos < 0: raise IndexError while not self._iter_done and tgt_pos >= len(self._tids): self._get_next_item() if tgt_pos >= len(self._tids): raise IndexError return pos def next_position(self, pos): return self._check_pos(pos + 1) def prev_position(self, pos): return self._check_pos(pos - 1) @property def focus(self): return self._focus def set_focus(self, pos): self._check_pos(pos) self._focus = pos self._modified() def _get_next_item(self): if self._iter_done: return None try: self._tids.append(next(self._threads)) except StopIteration: self._iter_done = True class SearchBuffer(Buffer): """shows a result list of threads for a query""" modename = 'search' sort_order = None reverse = None _message_count = None _thread_count = None def __init__(self, ui, initialquery='', sort_order=None, reverse = False): self.dbman = ui.dbman self.ui = ui self.querystring = initialquery default_order = SORT_NAME[settings.get('search_threads_sort_order')] self.sort_order = sort_order or default_order self.reverse = reverse self.rebuild() super().__init__() def __str__(self): ret = '[search] for "%s"' % self.querystring if self._thread_count is not None: threadstr = ' in %d thread%s' % (self._thread_count, '' if self._thread_count == 1 else 's') else: threadstr = '' if self._message_count is not None: ret += ' (%d message%s%s)' % (self._message_count, '' if self._message_count == 1 else 's', threadstr) return ret async def _calc_result_count(self): """ Asynchronously compute message/thread counts. Cache and return them. """ # instance variables can get reset if rebuild() gets called # while we are waiting, hence this code message_count = self._message_count thread_count = self._thread_count if message_count is None or thread_count is None: tasks = [self.ui._loop.run_in_executor(None, f, self.querystring) for f in (self.dbman.count_messages, self.dbman.count_threads)] message_count, thread_count = await asyncio.gather(*tasks) self._message_count = message_count self._thread_count = thread_count return message_count, thread_count async def get_info(self): message_count, thread_count = await self._calc_result_count() info = {} info['querystring'] = self.querystring info['result_count'] = message_count info['thread_count'] = thread_count info['result_count_positive'] = 's' if info['result_count'] > 1 else '' return info def rebuild(self): exclude_tags = settings.get_notmuch_setting('search', 'exclude_tags') if exclude_tags: exclude_tags = frozenset([t for t in exclude_tags.split(';') if t]) else: exclude_tags = frozenset() self._result_count = None self._thread_count = None threads = self.dbman.get_threads(self.querystring, self.sort_order, exclude_tags) wgt_factory = partial(ThreadlineWidget, dbman = self.dbman, query = self.querystring) threadlist = IterWalker(threads, wgt_factory, self.reverse) # check that the query is well-formed try: threadlist[0] except IndexError: # empty result list, no problem pass except QueryError: self.ui.notify('malformed query string: %s' % self.querystring, 'error') threadlist = [] self.threadlist = threadlist self.listbox = urwid.ListBox(self.threadlist) self.body = self.listbox def get_selected_threadline(self): """ returns curently focussed :class:`alot.widgets.ThreadlineWidget` from the result list. """ return self.threadlist[self.threadlist.focus] def get_selected_thread(self): """returns currently selected :class:`~alot.db.Thread`""" threadlinewidget = self.get_selected_threadline() thread = None if threadlinewidget: thread = threadlinewidget.get_thread() return thread def focus_first(self): self.body.set_focus(0) def focus_last(self): self.body.set_focus(len(self.threadlist) - 1)