From 1d74f717694a2e926891e6c59a9b82139a2164df Mon Sep 17 00:00:00 2001 From: Michael J Gruber Date: Fri, 16 Oct 2020 18:02:22 +0200 Subject: [PATCH 01/12] namedqueries: do not advertise filtfun namedqueries do not support filtering, so do not advertise this in the interface. --- alot/buffers/namedqueries.py | 3 +-- alot/commands/globals.py | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/alot/buffers/namedqueries.py b/alot/buffers/namedqueries.py index ad7c5fb69..8eea8e6f9 100644 --- a/alot/buffers/namedqueries.py +++ b/alot/buffers/namedqueries.py @@ -13,9 +13,8 @@ class NamedQueriesBuffer(Buffer): modename = 'namedqueries' - def __init__(self, ui, filtfun): + def __init__(self, ui): self.ui = ui - self.filtfun = filtfun self.isinitialized = False self.querylist = None self.rebuild() diff --git a/alot/commands/globals.py b/alot/commands/globals.py index aa1fd4368..bfda1762b 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -579,16 +579,13 @@ def apply(self, ui): @registerCommand(MODE, 'namedqueries') class NamedQueriesCommand(Command): """opens named queries buffer""" - def __init__(self, filtfun=bool, **kwargs): + def __init__(self, **kwargs): """ - :param filtfun: filter to apply to displayed list - :type filtfun: callable (str->bool) """ - self.filtfun = filtfun Command.__init__(self, **kwargs) def apply(self, ui): - ui.buffer_open(buffers.NamedQueriesBuffer(ui, self.filtfun)) + ui.buffer_open(buffers.NamedQueriesBuffer(ui)) @registerCommand(MODE, 'flush') From f40261a00c6f33318c89034cd3f9583c80358cb4 Mon Sep 17 00:00:00 2001 From: Michael J Gruber Date: Fri, 16 Oct 2020 18:07:54 +0200 Subject: [PATCH 02/12] allow to open multiple buffers of the same type We actively prevent opening multiple taglist or bufferlist buffers, but so far allow to open multiple named query buffers. They cannot differ, though, since there is nothing to configure about them. Rather than confusing the user by behaving differently for different buffer types, lift the restriction for all of them (but the bufferlist): at worst there are multiple instances of buffers with the same content. We do keep a single exception for the bufferlist buffer because that is the one you typically use to switch to other buffers. --- alot/commands/globals.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/alot/commands/globals.py b/alot/commands/globals.py index bfda1762b..beb2fe98c 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -566,14 +566,7 @@ def __init__(self, filtfun=lambda x: x, tags=None, **kwargs): def apply(self, ui): tags = self.tags or ui.dbman.get_all_tags() - blists = ui.get_buffers_of_type(buffers.TagListBuffer) - if blists: - buf = blists[0] - buf.tags = tags - buf.rebuild() - ui.buffer_focus(buf) - else: - ui.buffer_open(buffers.TagListBuffer(ui, tags, self.filtfun)) + ui.buffer_open(buffers.TagListBuffer(ui, tags, self.filtfun)) @registerCommand(MODE, 'namedqueries') From 91cbc4b76485fe52b73caeb2d1e3c2f027c99269 Mon Sep 17 00:00:00 2001 From: Michael J Gruber Date: Fri, 16 Oct 2020 18:18:58 +0200 Subject: [PATCH 03/12] taglist, bufferlist: provide defaults of correct signature The filtfun argument of these lists expects a boolean valued function of a single argument. The current default is `lambda x: x` which in most cases will result in no filtering, but may give the false impression that the filtfun argument is a transformation rather than a filter. Be explicit and specify the default no-op filter as `lambda x: True`. --- alot/buffers/bufferlist.py | 2 +- alot/buffers/taglist.py | 2 +- alot/commands/globals.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/alot/buffers/bufferlist.py b/alot/buffers/bufferlist.py index 14f94e205..ca524fc18 100644 --- a/alot/buffers/bufferlist.py +++ b/alot/buffers/bufferlist.py @@ -13,7 +13,7 @@ class BufferlistBuffer(Buffer): modename = 'bufferlist' - def __init__(self, ui, filtfun=lambda x: x): + def __init__(self, ui, filtfun=lambda x: True): self.filtfun = filtfun self.ui = ui self.isinitialized = False diff --git a/alot/buffers/taglist.py b/alot/buffers/taglist.py index 55fa2fddd..f926a77cf 100644 --- a/alot/buffers/taglist.py +++ b/alot/buffers/taglist.py @@ -13,7 +13,7 @@ class TagListBuffer(Buffer): modename = 'taglist' - def __init__(self, ui, alltags=None, filtfun=lambda x: x): + def __init__(self, ui, alltags=None, filtfun=lambda x: True): self.filtfun = filtfun self.ui = ui self.tags = alltags or [] diff --git a/alot/commands/globals.py b/alot/commands/globals.py index beb2fe98c..66afb8e83 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -532,7 +532,7 @@ def apply(self, ui): class OpenBufferlistCommand(Command): """open a list of active buffers""" - def __init__(self, filtfun=lambda x: x, **kwargs): + def __init__(self, filtfun=lambda x: True, **kwargs): """ :param filtfun: filter to apply to displayed list :type filtfun: callable (str->bool) @@ -555,7 +555,7 @@ def apply(self, ui): class TagListCommand(Command): """opens taglist buffer""" - def __init__(self, filtfun=lambda x: x, tags=None, **kwargs): + def __init__(self, filtfun=lambda x: True, tags=None, **kwargs): """ :param filtfun: filter to apply to displayed list :type filtfun: callable (str->bool) From 5acc65425931474b94d6dd65edf059e21e851567 Mon Sep 17 00:00:00 2001 From: Michael J Gruber Date: Sat, 2 Jan 2021 18:45:20 +0100 Subject: [PATCH 04/12] taglistbuffer: deal with 0 tags If a taglist buffer is asked to display 0 tags then it throws a ZeroDivisionError currently. Since people usually have at least 1 tag nobody noticed so far. But in the future the UI will allow to restrict the set of tags in a taglist buffer, so prepare for the case of 0 tags. --- alot/buffers/taglist.py | 5 +++-- alot/commands/taglist.py | 8 +++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/alot/buffers/taglist.py b/alot/buffers/taglist.py index f926a77cf..d7bb24e6d 100644 --- a/alot/buffers/taglist.py +++ b/alot/buffers/taglist.py @@ -51,7 +51,8 @@ def rebuild(self): self.taglist = urwid.ListBox(urwid.SimpleListWalker(lines)) self.body = self.taglist - self.taglist.set_focus(focusposition % len(displayedtags)) + if len(displayedtags): + self.taglist.set_focus(focusposition % len(displayedtags)) def focus_first(self): """Focus the first line in the tag list.""" @@ -64,7 +65,7 @@ def focus_last(self): self.body.set_focus(lastpos) def get_selected_tag(self): - """returns selected tagstring""" + """returns selected tagstring or throws AttributeError if none""" cols, _ = self.taglist.get_focus() tagwidget = cols.original_widget.get_focus() return tagwidget.tag diff --git a/alot/commands/taglist.py b/alot/commands/taglist.py index f5e8af73b..668655efb 100644 --- a/alot/commands/taglist.py +++ b/alot/commands/taglist.py @@ -2,6 +2,8 @@ # Copyright © 2018 Dylan Baker # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file +import logging + from . import Command, registerCommand from .globals import SearchCommand @@ -13,6 +15,10 @@ class TaglistSelectCommand(Command): """search for messages with selected tag""" async def apply(self, ui): - tagstring = ui.current_buffer.get_selected_tag() + try: + tagstring = ui.current_buffer.get_selected_tag() + except AttributeError: + logging.debug("taglist select without tag selection") + return cmd = SearchCommand(query=['tag:"%s"' % tagstring]) await ui.apply_command(cmd) From 9671cc6c6964561d8209beb7275a152697973c5d Mon Sep 17 00:00:00 2001 From: Michael J Gruber Date: Mon, 4 Jan 2021 14:49:10 +0100 Subject: [PATCH 05/12] taglist: do not quote tag unnecessarily Currently, selecting a tag searches for that tag surrounded by quotes. This results in a somewhat strange display in the resulting search buffer status line. Surround the tag by quotes only if it contains a space. We do the same when preparing the input line for `retag`, for example (where it is ncessary), and it gives a more natural display. --- alot/commands/taglist.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/alot/commands/taglist.py b/alot/commands/taglist.py index 668655efb..1c92176e8 100644 --- a/alot/commands/taglist.py +++ b/alot/commands/taglist.py @@ -20,5 +20,7 @@ async def apply(self, ui): except AttributeError: logging.debug("taglist select without tag selection") return - cmd = SearchCommand(query=['tag:"%s"' % tagstring]) + if ' ' in tagstring: + tagstring = '"%s"' % tagstring + cmd = SearchCommand(query=['tag:%s' % tagstring]) await ui.apply_command(cmd) From 64e52e31ee551e7a7603595700e52ded31e5959a Mon Sep 17 00:00:00 2001 From: Michael J Gruber Date: Fri, 16 Oct 2020 21:12:10 +0200 Subject: [PATCH 06/12] taglist: match by regular expression Currently, there is no way to specify the taglist filter function from UI, and the tags argument just allows to display the list of tags that you specify. Introduce a match argument that allows to list those tags matching the specified regular expression. Especially useful if you bind a key to `prompt 'taglist '`. --- alot/commands/globals.py | 14 ++++++++++++-- docs/source/usage/modes/global.rst | 3 +++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/alot/commands/globals.py b/alot/commands/globals.py index 66afb8e83..d2184aef2 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -7,6 +7,7 @@ import email import email.utils import glob +import re import logging import os import subprocess @@ -551,16 +552,25 @@ def apply(self, ui): @registerCommand(MODE, 'taglist', arguments=[ (['--tags'], {'nargs': '+', 'help': 'tags to display'}), + (['match'], {'nargs': '?', + 'help': 'regular expression to match tags against'}), ]) class TagListCommand(Command): """opens taglist buffer""" - def __init__(self, filtfun=lambda x: True, tags=None, **kwargs): + + def __init__(self, filtfun=lambda x: True, tags=None, match=None, **kwargs): """ :param filtfun: filter to apply to displayed list :type filtfun: callable (str->bool) + :param match: regular expression to match tags against + :type match: string """ - self.filtfun = filtfun + if match: + pattern = re.compile(match) + self.filtfun = lambda x: pattern.search(x) is not None + else: + self.filtfun = filtfun self.tags = tags Command.__init__(self, **kwargs) diff --git a/docs/source/usage/modes/global.rst b/docs/source/usage/modes/global.rst index 955b6ffe1..02d21935e 100644 --- a/docs/source/usage/modes/global.rst +++ b/docs/source/usage/modes/global.rst @@ -226,6 +226,9 @@ The following commands are available globally: opens taglist buffer + argument + regular expression to match tags against + optional arguments :---tags: tags to display From d0ce11c05de99051fee0777873b8b3edcb3b8a5d Mon Sep 17 00:00:00 2001 From: Michael J Gruber Date: Sat, 17 Oct 2020 17:16:36 +0200 Subject: [PATCH 07/12] taglist: restrict tags to displayed messages by default Currently, the taglist command runs on the global list of tags. Change the default behaviour to restrict the taglist to those appearing in the displayed messages of a search or thread buffer. This is basically the same list which `notmuch search --output=tags` would return for the query underlying the search buffer resp. the query for the thread displayed in a thread buffer. Introduce a flag `--global` which allows to list all tags globally. Binding a key to `taglist --global` restores the previous default behaviour. --- alot/commands/globals.py | 17 +++++++++++++++-- alot/db/manager.py | 10 ++++++++++ docs/source/usage/modes/global.rst | 1 + 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/alot/commands/globals.py b/alot/commands/globals.py index d2184aef2..ea160be01 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -551,6 +551,9 @@ def apply(self, ui): @registerCommand(MODE, 'taglist', arguments=[ + (['--global'], {'action': 'store_true', + 'help': 'list all tags globally instead of just those from the buffer', + 'dest': 'globally'}), (['--tags'], {'nargs': '+', 'help': 'tags to display'}), (['match'], {'nargs': '?', 'help': 'regular expression to match tags against'}), @@ -559,23 +562,33 @@ class TagListCommand(Command): """opens taglist buffer""" - def __init__(self, filtfun=lambda x: True, tags=None, match=None, **kwargs): + def __init__(self, filtfun=lambda x: True, tags=None, match=None, globally=False, **kwargs): """ :param filtfun: filter to apply to displayed list :type filtfun: callable (str->bool) :param match: regular expression to match tags against :type match: string + :param globally: list all tags globally instead of just those from the buffer + :type globally: bool """ if match: pattern = re.compile(match) self.filtfun = lambda x: pattern.search(x) is not None else: self.filtfun = filtfun + self.globally = globally self.tags = tags Command.__init__(self, **kwargs) def apply(self, ui): - tags = self.tags or ui.dbman.get_all_tags() + if self.tags: + tags = self.tags + elif (not self.globally) and isinstance(ui.current_buffer, buffers.SearchBuffer): + tags = ui.dbman.collect_tags(ui.current_buffer.querystring) + elif (not self.globally) and isinstance(ui.current_buffer, buffers.ThreadBuffer): + tags = list(ui.current_buffer.thread.get_tags()) + else: # self.globally or otherBuffer + tags = ui.dbman.get_all_tags() ui.buffer_open(buffers.TagListBuffer(ui, tags, self.filtfun)) diff --git a/alot/db/manager.py b/alot/db/manager.py index 247f28a91..a2c09436b 100644 --- a/alot/db/manager.py +++ b/alot/db/manager.py @@ -238,6 +238,16 @@ def count_messages(self, querystring): return db.count_messages(querystring, exclude_tags=settings.get('exclude_tags')) + def collect_tags(self, querystring): + """returns tags of messages that match `querystring`""" + db = Database(path=self.path, mode=Database.MODE.READ_ONLY) + tagset = notmuch2._tags.ImmutableTagSet( + db.messages(querystring, + exclude_tags=settings.get('exclude_tags')), + '_iter_p', + notmuch2.capi.lib.notmuch_messages_collect_tags) + return [t for t in tagset] + def count_threads(self, querystring): """returns number of threads that match `querystring`""" db = Database(path=self.path, mode=Database.MODE.READ_ONLY) diff --git a/docs/source/usage/modes/global.rst b/docs/source/usage/modes/global.rst index 02d21935e..52e29de85 100644 --- a/docs/source/usage/modes/global.rst +++ b/docs/source/usage/modes/global.rst @@ -230,5 +230,6 @@ The following commands are available globally: regular expression to match tags against optional arguments + :---global: list all tags globally instead of just those from the buffer :---tags: tags to display From 24276ce04f91b860e4019c3bb22df4c5988eb108 Mon Sep 17 00:00:00 2001 From: Michael J Gruber Date: Sat, 17 Oct 2020 17:37:56 +0200 Subject: [PATCH 08/12] taglist: refine search from buffer When a taglist buffer lists tags from a search or thead buffer (without `--global`) then selecting a tag lists all messages in the db with that tag. This feels counter-intuitive. Instead, remember the original query and refine the query by filtering on the tag in addition to the original query when the tag is `select`ed. In addition, introduce a new command `globalselect` which does a global search with the selected tag. The default key binding is `meta enter`, complementing the default binding `enter` for `select`. --- alot/buffers/taglist.py | 3 ++- alot/commands/globals.py | 6 +++++- alot/commands/taglist.py | 23 ++++++++++++++++++++++- alot/defaults/default.bindings | 1 + docs/source/usage/modes/taglist.rst | 9 ++++++++- 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/alot/buffers/taglist.py b/alot/buffers/taglist.py index d7bb24e6d..4ff90dfd8 100644 --- a/alot/buffers/taglist.py +++ b/alot/buffers/taglist.py @@ -13,10 +13,11 @@ class TagListBuffer(Buffer): modename = 'taglist' - def __init__(self, ui, alltags=None, filtfun=lambda x: True): + def __init__(self, ui, alltags=None, filtfun=lambda x: True, querystring=None): self.filtfun = filtfun self.ui = ui self.tags = alltags or [] + self.querystring = querystring self.isinitialized = False self.rebuild() Buffer.__init__(self, ui, self.body) diff --git a/alot/commands/globals.py b/alot/commands/globals.py index ea160be01..f473b40f1 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -581,15 +581,19 @@ def __init__(self, filtfun=lambda x: True, tags=None, match=None, globally=False Command.__init__(self, **kwargs) def apply(self, ui): + querystring = None if self.tags: tags = self.tags elif (not self.globally) and isinstance(ui.current_buffer, buffers.SearchBuffer): tags = ui.dbman.collect_tags(ui.current_buffer.querystring) + querystring = ui.current_buffer.querystring elif (not self.globally) and isinstance(ui.current_buffer, buffers.ThreadBuffer): tags = list(ui.current_buffer.thread.get_tags()) + querystring = 'thread:%s' % ui.current_buffer.thread.get_thread_id() else: # self.globally or otherBuffer tags = ui.dbman.get_all_tags() - ui.buffer_open(buffers.TagListBuffer(ui, tags, self.filtfun)) + ui.buffer_open(buffers.TagListBuffer( + ui, tags, self.filtfun, querystring)) @registerCommand(MODE, 'namedqueries') diff --git a/alot/commands/taglist.py b/alot/commands/taglist.py index 1c92176e8..3c1dc42de 100644 --- a/alot/commands/taglist.py +++ b/alot/commands/taglist.py @@ -13,7 +13,7 @@ @registerCommand(MODE, 'select') class TaglistSelectCommand(Command): - """search for messages with selected tag""" + """search for messages with selected tag within original buffer""" async def apply(self, ui): try: tagstring = ui.current_buffer.get_selected_tag() @@ -22,5 +22,26 @@ async def apply(self, ui): return if ' ' in tagstring: tagstring = '"%s"' % tagstring + querystring = ui.current_buffer.querystring + if querystring: + fullquerystring = '(%s) AND tag:%s' % (querystring, tagstring) + else: + fullquerystring = 'tag:%s' % tagstring + cmd = SearchCommand(query=[fullquerystring]) + await ui.apply_command(cmd) + + +@registerCommand(MODE, 'globalselect') +class TaglistGlobalSelectCommand(Command): + + """search for messages with selected tag""" + async def apply(self, ui): + try: + tagstring = ui.current_buffer.get_selected_tag() + except AttributeError: + logging.debug("taglist globalselect without tag selection") + return + if ' ' in tagstring: + tagstring = '"%s"' % tagstring cmd = SearchCommand(query=['tag:%s' % tagstring]) await ui.apply_command(cmd) diff --git a/alot/defaults/default.bindings b/alot/defaults/default.bindings index ffca1bdc9..6688ef1bc 100644 --- a/alot/defaults/default.bindings +++ b/alot/defaults/default.bindings @@ -57,6 +57,7 @@ q = exit [taglist] enter = select + meta enter = globalselect [namedqueries] enter = select diff --git a/docs/source/usage/modes/taglist.rst b/docs/source/usage/modes/taglist.rst index 08917bb2b..c1eed6f48 100644 --- a/docs/source/usage/modes/taglist.rst +++ b/docs/source/usage/modes/taglist.rst @@ -5,10 +5,17 @@ Commands in 'taglist' mode -------------------------- The following commands are available in taglist mode: +.. _cmd.taglist.globalselect: + +.. describe:: globalselect + + search for messages with selected tag + + .. _cmd.taglist.select: .. describe:: select - search for messages with selected tag + search for messages with selected tag within original buffer From 5a2b447458531c5160258bfd7e5b8e2ba3a08d75 Mon Sep 17 00:00:00 2001 From: Michael J Gruber Date: Thu, 31 Dec 2020 12:06:53 +0100 Subject: [PATCH 09/12] taglist buffer: make statusbar informative Until recently, taglist buffers were global. Now they can differ by realm (global, search, thread) and pattern matching on the tags. Provide this information for the statusbar and update the defaults to display it. In addition, provide a count for the messages matching the specific query, i.e. globally or the query from the search or thread buffer, restricted to matches on the pattern matched tags. Note that even in the global case without pattern match the matched count is typically lower than the total count since the latter includes messages with excluded tags as well as those without any tags, whereas the former list any not excluded message with any tag. --- alot/buffers/taglist.py | 41 ++++++++++++++++++++++++-- alot/commands/globals.py | 3 +- alot/defaults/alot.rc.spec | 11 +++++-- docs/source/configuration/alotrc_table | 11 +++++-- 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/alot/buffers/taglist.py b/alot/buffers/taglist.py index 4ff90dfd8..75ff366e6 100644 --- a/alot/buffers/taglist.py +++ b/alot/buffers/taglist.py @@ -2,6 +2,7 @@ # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file import urwid +from notmuch2 import NotmuchError from .buffer import Buffer from ..settings.const import settings @@ -13,15 +14,30 @@ class TagListBuffer(Buffer): modename = 'taglist' - def __init__(self, ui, alltags=None, filtfun=lambda x: True, querystring=None): + def __init__(self, ui, alltags=None, filtfun=lambda x: True, querystring=None, match=None): self.filtfun = filtfun self.ui = ui self.tags = alltags or [] self.querystring = querystring + self.match = match + self.result_count = 0 self.isinitialized = False self.rebuild() Buffer.__init__(self, ui, self.body) + def __str__(self): + formatstring = '[taglist] for "%s matching %s" (%d message%s)' + return formatstring % (self.querystring or '*', self.match or '*', self.result_count, + 's' if self.result_count > 1 else '') + + def get_info(self): + info = {} + info['querystring'] = self.querystring or '*' + info['match'] = self.match or '*' + info['result_count'] = self.result_count + info['result_count_positive'] = 's' if self.result_count > 1 else '' + return info + def rebuild(self): if self.isinitialized: focusposition = self.taglist.get_focus()[1] @@ -29,9 +45,30 @@ def rebuild(self): focusposition = 0 self.isinitialized = True - lines = list() displayedtags = sorted((t for t in self.tags if self.filtfun(t)), key=str.lower) + + exclude_tags = settings.get_notmuch_setting('search', 'exclude_tags') + if exclude_tags: + exclude_tags = [t for t in exclude_tags.split(';') if t] + + compoundquerystring = ' AND '.join(['(%s)' % q for q in + [self.querystring, + ' OR '.join(['tag:"%s"' % t for t + in displayedtags])] + if q]) + + try: + self.result_count = self.ui.dbman.count_messages( + compoundquerystring or '*') + except NotmuchError: + self.ui.notify('malformed query string: %s' % compoundquerystring, + 'error') + self.taglist = urwid.ListBox([]) + self.body = self.listbox + return + + lines = list() for (num, b) in enumerate(displayedtags): if (num % 2) == 0: attr = settings.get_theming_attribute('taglist', 'line_even') diff --git a/alot/commands/globals.py b/alot/commands/globals.py index f473b40f1..e9a6e3944 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -577,6 +577,7 @@ def __init__(self, filtfun=lambda x: True, tags=None, match=None, globally=False else: self.filtfun = filtfun self.globally = globally + self.match = match self.tags = tags Command.__init__(self, **kwargs) @@ -593,7 +594,7 @@ def apply(self, ui): else: # self.globally or otherBuffer tags = ui.dbman.get_all_tags() ui.buffer_open(buffers.TagListBuffer( - ui, tags, self.filtfun, querystring)) + ui, tags, self.filtfun, querystring, self.match)) @registerCommand(MODE, 'namedqueries') diff --git a/alot/defaults/alot.rc.spec b/alot/defaults/alot.rc.spec index 2d28ad391..c092900d2 100644 --- a/alot/defaults/alot.rc.spec +++ b/alot/defaults/alot.rc.spec @@ -155,9 +155,14 @@ thread_statusbar = mixed_list(string, string, default=list('[{buffer_no}: thread # Format of the status-bar in taglist mode. # This is a pair of strings to be left and right aligned in the status-bar. -# These strings may contain variables listed at :ref:`bufferlist_statusbar ` -# that will be substituted accordingly. -taglist_statusbar = mixed_list(string, string, default=list('[{buffer_no}: taglist]','{input_queue} total messages: {total_messages}')) +# Apart from the global variables listed at :ref:`bufferlist_statusbar ` +# these strings may contain variables: +# +# * `{querystring}`: search string +# * `{match}`: match expression +# * `{result_count}`: number of matching messages +# * `{result_count_positive}`: 's' if result count is greater than 0. +taglist_statusbar = mixed_list(string, string, default=list('[{buffer_no}: taglist] for "{querystring}" matching "{match}"','{input_queue} {result_count} of {total_messages} messages')) # Format of the status-bar in named query list mode. # This is a pair of strings to be left and right aligned in the status-bar. diff --git a/docs/source/configuration/alotrc_table b/docs/source/configuration/alotrc_table index e856fcb36..09ce486b8 100644 --- a/docs/source/configuration/alotrc_table +++ b/docs/source/configuration/alotrc_table @@ -635,11 +635,16 @@ Format of the status-bar in taglist mode. This is a pair of strings to be left and right aligned in the status-bar. - These strings may contain variables listed at :ref:`bufferlist_statusbar ` - that will be substituted accordingly. + Apart from the global variables listed at :ref:`bufferlist_statusbar ` + these strings may contain variables: + + * `{querystring}`: search string + * `{match}`: match expression + * `{result_count}`: number of matching messages + * `{result_count_positive}`: 's' if result count is greater than 0. :type: mixed_list - :default: [{buffer_no}: taglist], {input_queue} total messages: {total_messages} + :default: [{buffer_no}: taglist] for "{querystring}" matching "{match}", {input_queue} {result_count} of {total_messages} messages .. _template-dir: From e2f609cfda6c7c5e5eba64b6073379c9a6af0a79 Mon Sep 17 00:00:00 2001 From: Michael J Gruber Date: Thu, 31 Dec 2020 13:24:22 +0100 Subject: [PATCH 10/12] refactor taglist/TagWidget taglist jumps through some hoops to amend the tags in the list by additional information (hidden, name before translation) without urwids column spreading kicking in, when the aim is in fact to add that text to the tag text. Refactor TagWidget so that callers can ask the widget to amend the information. The default is off for callers like the search and thread widgets. --- alot/buffers/taglist.py | 7 +------ alot/widgets/globals.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/alot/buffers/taglist.py b/alot/buffers/taglist.py index 75ff366e6..5d1929f4f 100644 --- a/alot/buffers/taglist.py +++ b/alot/buffers/taglist.py @@ -76,12 +76,7 @@ def rebuild(self): attr = settings.get_theming_attribute('taglist', 'line_odd') focus_att = settings.get_theming_attribute('taglist', 'line_focus') - tw = TagWidget(b, attr, focus_att) - rows = [('fixed', tw.width(), tw)] - if tw.hidden: - rows.append(urwid.Text(b + ' [hidden]')) - elif tw.translated is not b: - rows.append(urwid.Text('(%s)' % b)) + rows = [TagWidget(b, attr, focus_att, True)] line = urwid.Columns(rows, dividechars=1) line = urwid.AttrMap(line, attr, focus_att) lines.append(line) diff --git a/alot/widgets/globals.py b/alot/widgets/globals.py index 9de9cbbbc..68775b12b 100644 --- a/alot/widgets/globals.py +++ b/alot/widgets/globals.py @@ -287,14 +287,21 @@ class TagWidget(urwid.AttrMap): :type tag: str """ - def __init__(self, tag, fallback_normal=None, fallback_focus=None): + def __init__(self, tag, fallback_normal=None, fallback_focus=None, + amend=False): 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') + txt = self.translated + if amend: + if self.hidden: + txt += self.tag + ' [hidden]' + elif self.translated is not self.tag: + txt += ' (%s)' % self.tag + self.txt = urwid.Text(txt, wrap='clip') self.__hash = hash((self.translated, self.txt)) normal_att = representation['normal'] focus_att = representation['focussed'] From 158f8988f6dfeb6c97b0008995454f56f1aef538 Mon Sep 17 00:00:00 2001 From: Michael J Gruber Date: Thu, 31 Dec 2020 13:27:29 +0100 Subject: [PATCH 11/12] taglist: show message counts So far, the taglist shows a list of tags (no surprise here) only, recently with a total count of matched message. Show a count for each tag analogous to the namelist widget: These are the counts for original query (global, search, or thread) AND the tag, and also for that AND tag:unread. --- alot/buffers/taglist.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/alot/buffers/taglist.py b/alot/buffers/taglist.py index 5d1929f4f..0d5c48d29 100644 --- a/alot/buffers/taglist.py +++ b/alot/buffers/taglist.py @@ -77,6 +77,13 @@ def rebuild(self): focus_att = settings.get_theming_attribute('taglist', 'line_focus') rows = [TagWidget(b, attr, focus_att, True)] + + count = self.ui.dbman.count_messages(' AND '.join(['(%s)' % q for q in + [self.querystring, 'tag:"%s"' % b] if q])) + count_unread = self.ui.dbman.count_messages(' AND '.join(['(%s)' % q for q in + [self.querystring, 'tag:"%s"' % b, 'tag:unread'] if q])) + rows.append(urwid.Text('{0:>7} {1:7}'. + format(count, '({0})'.format(count_unread)))) line = urwid.Columns(rows, dividechars=1) line = urwid.AttrMap(line, attr, focus_att) lines.append(line) From 4a62d59894892f5d644a8d3aca68214a56105651 Mon Sep 17 00:00:00 2001 From: Michael J Gruber Date: Thu, 31 Dec 2020 14:25:56 +0100 Subject: [PATCH 12/12] taglist: implement untag command The usual untag command requires an argument. In the taglist, we have a a shortcut available since a tag is selected. Use that for quickly removing tags. --- alot/buffers/taglist.py | 10 +++++++- alot/commands/taglist.py | 37 +++++++++++++++++++++++++++++ docs/source/usage/modes/taglist.rst | 7 ++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/alot/buffers/taglist.py b/alot/buffers/taglist.py index 0d5c48d29..864881f22 100644 --- a/alot/buffers/taglist.py +++ b/alot/buffers/taglist.py @@ -104,8 +104,16 @@ def focus_last(self): lastpos = allpos[0] self.body.set_focus(lastpos) + def get_selected_tagline(self): + """ + returns curently focussed :class:`urwid.AttrMap` tagline widget + from the result list. + """ + cols, _ = self.taglist.get_focus() + return cols + def get_selected_tag(self): """returns selected tagstring or throws AttributeError if none""" - cols, _ = self.taglist.get_focus() + cols = self.get_selected_tagline() tagwidget = cols.original_widget.get_focus() return tagwidget.tag diff --git a/alot/commands/taglist.py b/alot/commands/taglist.py index 3c1dc42de..f257efd2b 100644 --- a/alot/commands/taglist.py +++ b/alot/commands/taglist.py @@ -6,6 +6,7 @@ from . import Command, registerCommand from .globals import SearchCommand +from .. import commands MODE = 'taglist' @@ -45,3 +46,39 @@ async def apply(self, ui): tagstring = '"%s"' % tagstring cmd = SearchCommand(query=['tag:%s' % tagstring]) await ui.apply_command(cmd) + + +@registerCommand(MODE, 'untag') +class UntagCommand(Command): + + """remove selected tag from all messages within original buffer""" + async def apply(self, ui): + taglistbuffer = ui.current_buffer + taglinewidget = taglistbuffer.get_selected_tagline() + try: + tag = taglistbuffer.get_selected_tag() + except AttributeError: + logging.debug("taglist untag without tag selection") + return + tagstring = 'tag:"%s"' % tag + querystring = taglistbuffer.querystring + if querystring: + fullquerystring = '(%s) AND %s' % (querystring, tagstring) + else: + fullquerystring = tagstring + + def refresh(): + if taglinewidget in taglistbuffer.taglist: + taglistbuffer.taglist.remove(taglinewidget) + if tag in taglistbuffer.tags: + taglistbuffer.tags.remove(tag) + taglistbuffer.rebuild() + ui.update() + + try: + ui.dbman.untag(fullquerystring, [tag]) + except DatabaseROError: + ui.notify('index in read-only mode', priority='error') + return + + await ui.apply_command(commands.globals.FlushCommand(callback=refresh)) diff --git a/docs/source/usage/modes/taglist.rst b/docs/source/usage/modes/taglist.rst index c1eed6f48..4142e6e00 100644 --- a/docs/source/usage/modes/taglist.rst +++ b/docs/source/usage/modes/taglist.rst @@ -19,3 +19,10 @@ The following commands are available in taglist mode: search for messages with selected tag within original buffer +.. _cmd.taglist.untag: + +.. describe:: untag + + remove selected tag from all messages within original buffer + +