Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new buffer to search and display messages #896

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions alot/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
236 changes: 232 additions & 4 deletions alot/buffers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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()
1 change: 1 addition & 0 deletions alot/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class CommandCanceled(Exception):

COMMANDS = {
'search': {},
'searchmessages': {},
'envelope': {},
'bufferlist': {},
'taglist': {},
Expand Down
31 changes: 31 additions & 0 deletions alot/commands/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
23 changes: 23 additions & 0 deletions alot/commands/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']}),
Expand Down
Loading