diff options
author | Dylan Baker <dylan@pnwbakers.com> | 2018-07-24 14:41:22 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-07-24 14:41:22 -0700 |
commit | 4cba47a8143059a68c904e40e575fc4d493bf6de (patch) | |
tree | 8800830a4dea35c17b951984b88c39dfafd47a09 /alot | |
parent | bf891cec60ea4b535261d976a124bd5b683ce437 (diff) | |
parent | 6ab2f27b2c15aab4f340b7770598235b155a06b7 (diff) |
Merge pull request #1256 from pazz/namedqueries
New buffer type for notmuch's named query strings
Diffstat (limited to 'alot')
-rw-r--r-- | alot/__main__.py | 3 | ||||
-rw-r--r-- | alot/buffers/__init__.py | 1 | ||||
-rw-r--r-- | alot/buffers/namedqueries.py | 78 | ||||
-rw-r--r-- | alot/commands/__init__.py | 1 | ||||
-rw-r--r-- | alot/commands/globals.py | 106 | ||||
-rw-r--r-- | alot/commands/namedqueries.py | 30 | ||||
-rw-r--r-- | alot/commands/search.py | 22 | ||||
-rw-r--r-- | alot/completion.py | 23 | ||||
-rw-r--r-- | alot/db/manager.py | 46 | ||||
-rw-r--r-- | alot/defaults/alot.rc.spec | 6 | ||||
-rw-r--r-- | alot/defaults/default.bindings | 3 | ||||
-rw-r--r-- | alot/defaults/default.theme | 4 | ||||
-rw-r--r-- | alot/defaults/theme.spec | 4 | ||||
-rw-r--r-- | alot/widgets/namedqueries.py | 30 |
14 files changed, 353 insertions, 4 deletions
diff --git a/alot/__main__.py b/alot/__main__.py index 257eb051..0d100134 100644 --- a/alot/__main__.py +++ b/alot/__main__.py @@ -17,7 +17,8 @@ from alot.commands import CommandParseError, COMMANDS from alot.utils import argparse as cargparse -_SUBCOMMANDS = ['search', 'compose', 'bufferlist', 'taglist', 'pyshell'] +_SUBCOMMANDS = ['search', 'compose', 'bufferlist', 'taglist', 'namedqueries', + 'pyshell'] def parser(): diff --git a/alot/buffers/__init__.py b/alot/buffers/__init__.py index 2b52a544..bd503f17 100644 --- a/alot/buffers/__init__.py +++ b/alot/buffers/__init__.py @@ -8,3 +8,4 @@ from .envelope import EnvelopeBuffer from .search import SearchBuffer from .taglist import TagListBuffer from .thread import ThreadBuffer +from .namedqueries import NamedQueriesBuffer diff --git a/alot/buffers/namedqueries.py b/alot/buffers/namedqueries.py new file mode 100644 index 00000000..ad7c5fb6 --- /dev/null +++ b/alot/buffers/namedqueries.py @@ -0,0 +1,78 @@ +# Copyright (C) 2011-2018 Patrick Totzke <patricktotzke@gmail.com> +# This file is released under the GNU GPL, version 3 or a later revision. +# For further details see the COPYING file +import urwid + +from .buffer import Buffer +from ..settings.const import settings +from ..widgets.namedqueries import QuerylineWidget + + +class NamedQueriesBuffer(Buffer): + """lists named queries present in the notmuch database""" + + modename = 'namedqueries' + + def __init__(self, ui, filtfun): + self.ui = ui + self.filtfun = filtfun + self.isinitialized = False + self.querylist = None + self.rebuild() + Buffer.__init__(self, ui, self.body) + + def rebuild(self): + self.queries = self.ui.dbman.get_named_queries() + + if self.isinitialized: + focusposition = self.querylist.get_focus()[1] + else: + focusposition = 0 + + lines = [] + for (num, key) in enumerate(self.queries): + value = self.queries[key] + count = self.ui.dbman.count_messages('query:"%s"' % key) + count_unread = self.ui.dbman.count_messages('query:"%s" and ' + 'tag:unread' % key) + line = QuerylineWidget(key, value, count, count_unread) + + if (num % 2) == 0: + attr = settings.get_theming_attribute('namedqueries', + 'line_even') + else: + attr = settings.get_theming_attribute('namedqueries', + 'line_odd') + focus_att = settings.get_theming_attribute('namedqueries', + 'line_focus') + + line = urwid.AttrMap(line, attr, focus_att) + lines.append(line) + + self.querylist = urwid.ListBox(urwid.SimpleListWalker(lines)) + self.body = self.querylist + + self.querylist.set_focus(focusposition % len(self.queries)) + + self.isinitialized = True + + def focus_first(self): + """Focus the first line in the query list.""" + self.body.set_focus(0) + + def focus_last(self): + allpos = self.querylist.body.positions(reverse=True) + if allpos: + lastpos = allpos[0] + self.body.set_focus(lastpos) + + def get_selected_query(self): + """returns selected query""" + return self.querylist.get_focus()[0].original_widget.query + + def get_info(self): + info = {} + + info['query_count'] = len(self.queries) + + return info diff --git a/alot/commands/__init__.py b/alot/commands/__init__.py index ddd7ed17..c1662073 100644 --- a/alot/commands/__init__.py +++ b/alot/commands/__init__.py @@ -38,6 +38,7 @@ COMMANDS = { 'envelope': {}, 'bufferlist': {}, 'taglist': {}, + 'namedqueries': {}, 'thread': {}, 'global': {}, } diff --git a/alot/commands/globals.py b/alot/commands/globals.py index 06d0d5ce..0f1d6e26 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -553,6 +553,21 @@ class TagListCommand(Command): ui.buffer_open(buffers.TagListBuffer(ui, tags, self.filtfun)) +@registerCommand(MODE, 'namedqueries') +class NamedQueriesCommand(Command): + """opens named queries buffer""" + def __init__(self, filtfun=bool, **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)) + + @registerCommand(MODE, 'flush') class FlushCommand(Command): @@ -979,3 +994,94 @@ class ReloadCommand(Command): except ConfigError as e: ui.notify('Error when reloading config files:\n {}'.format(e), priority='error') + + +@registerCommand( + MODE, 'savequery', + arguments=[ + (['--no-flush'], {'action': 'store_false', 'dest': 'flush', + 'default': 'True', + 'help': 'postpone a writeout to the index'}), + (['alias'], {'help': 'alias to use for query string'}), + (['query'], {'help': 'query string to store', + 'nargs': '+'}) + ], + help='store query string as a "named query" in the database') +class SaveQueryCommand(Command): + + """save alias for query string""" + repeatable = False + + def __init__(self, alias, query=None, flush=True, **kwargs): + """ + :param alias: name to use for query string + :type alias: str + :param query: query string to save + :type query: str or None + :param flush: immediately write out to the index + :type flush: bool + """ + self.alias = alias + if query is None: + self.query = '' + else: + self.query = ' '.join(query) + self.flush = flush + Command.__init__(self, **kwargs) + + def apply(self, ui): + msg = 'saved alias "%s" for query string "%s"' % (self.alias, + self.query) + + try: + ui.dbman.save_named_query(self.alias, self.query) + logging.debug(msg) + ui.notify(msg) + except DatabaseROError: + ui.notify('index in read-only mode', priority='error') + return + + # flush index + if self.flush: + ui.apply_command(commands.globals.FlushCommand()) + + +@registerCommand( + MODE, 'removequery', + arguments=[ + (['--no-flush'], {'action': 'store_false', 'dest': 'flush', + 'default': 'True', + 'help': 'postpone a writeout to the index'}), + (['alias'], {'help': 'alias to remove'}), + ], + help='removes a "named query" from the database') +class RemoveQueryCommand(Command): + + """remove named query string for given alias""" + repeatable = False + + def __init__(self, alias, flush=True, **kwargs): + """ + :param alias: name to use for query string + :type alias: str + :param flush: immediately write out to the index + :type flush: bool + """ + self.alias = alias + self.flush = flush + Command.__init__(self, **kwargs) + + def apply(self, ui): + msg = 'removed alias "%s"' % (self.alias) + + try: + ui.dbman.remove_named_query(self.alias) + logging.debug(msg) + ui.notify(msg) + except DatabaseROError: + ui.notify('index in read-only mode', priority='error') + return + + # flush index + if self.flush: + ui.apply_command(commands.globals.FlushCommand()) diff --git a/alot/commands/namedqueries.py b/alot/commands/namedqueries.py new file mode 100644 index 00000000..362e2bb6 --- /dev/null +++ b/alot/commands/namedqueries.py @@ -0,0 +1,30 @@ +# Copyright (C) 2011-2018 Patrick Totzke <patricktotzke@gmail.com> +# This file is released under the GNU GPL, version 3 or a later revision. +# For further details see the COPYING file +import argparse + +from . import Command, registerCommand +from .globals import SearchCommand + +MODE = 'namedqueries' + + +@registerCommand(MODE, 'select', arguments=[ + (['filt'], {'nargs': argparse.REMAINDER, + 'help': 'additional filter to apply to query'}), +]) +class NamedqueriesSelectCommand(Command): + + """search for messages with selected query""" + def __init__(self, filt=None, **kwargs): + self._filt = filt + Command.__init__(self, **kwargs) + + def apply(self, ui): + query_name = ui.current_buffer.get_selected_query() + query = ['query:"%s"' % query_name] + if self._filt: + query.extend(['and'] + self._filt) + + cmd = SearchCommand(query=query) + ui.apply_command(cmd) diff --git a/alot/commands/search.py b/alot/commands/search.py index 3e172dd5..d1dccd71 100644 --- a/alot/commands/search.py +++ b/alot/commands/search.py @@ -7,6 +7,7 @@ import logging from . import Command, registerCommand from .globals import PromptCommand from .globals import MoveCommand +from .globals import SaveQueryCommand as GlobalSaveQueryCommand from .common import RetagPromptCommand from .. import commands @@ -241,3 +242,24 @@ class MoveFocusCommand(MoveCommand): ui.update() else: MoveCommand.apply(self, ui) + + +@registerCommand( + MODE, 'savequery', + arguments=[ + (['--no-flush'], {'action': 'store_false', 'dest': 'flush', + 'default': 'True', + 'help': 'postpone a writeout to the index'}), + (['alias'], {'help': 'alias to use for query string'}), + (['query'], {'help': 'query string to store', + 'nargs': argparse.REMAINDER, + }), + ], + help='store query string as a "named query" in the database. ' + 'This falls back to the current search query in search buffers.') +class SaveQueryCommand(GlobalSaveQueryCommand): + def apply(self, ui): + searchbuffer = ui.current_buffer + if not self.query: + self.query = searchbuffer.querystring + GlobalSaveQueryCommand.apply(self, ui) diff --git a/alot/completion.py b/alot/completion.py index a14c2ae0..8eb46448 100644 --- a/alot/completion.py +++ b/alot/completion.py @@ -123,6 +123,19 @@ class MultipleSelectionCompleter(Completer): return res +class NamedQueryCompleter(StringlistCompleter): + """complete the name of a named query string""" + + def __init__(self, dbman): + """ + :param dbman: used to look up named query strings in the DB + :type dbman: :class:`~alot.db.DBManager` + """ + # mapping of alias to query string (dict str -> str) + nqueries = dbman.get_named_queries() + StringlistCompleter.__init__(self, list(nqueries)) + + class QueryCompleter(Completer): """completion for a notmuch query string""" def __init__(self, dbman): @@ -134,19 +147,23 @@ class QueryCompleter(Completer): abooks = settings.get_addressbooks() self._abookscompleter = AbooksCompleter(abooks, addressesonly=True) self._tagcompleter = TagCompleter(dbman) + self._nquerycompleter = NamedQueryCompleter(dbman) self.keywords = ['tag', 'from', 'to', 'subject', 'attachment', - 'is', 'id', 'thread', 'folder'] + 'is', 'id', 'thread', 'folder', 'query'] def complete(self, original, pos): mypart, start, end, mypos = self.relevant_part(original, pos) myprefix = mypart[:mypos] - m = re.search(r'(tag|is|to|from):(\w*)', myprefix) + m = re.search(r'(tag|is|to|from|query):(\w*)', myprefix) if m: cmd, _ = m.groups() - cmdlen = len(cmd) + 1 # length of the keyword part incld colon + cmdlen = len(cmd) + 1 # length of the keyword part including colon if cmd in ['to', 'from']: localres = self._abookscompleter.complete(mypart[cmdlen:], mypos - cmdlen) + elif cmd in ['query']: + localres = self._nquerycompleter.complete(mypart[cmdlen:], + mypos - cmdlen) else: localres = self._tagcompleter.complete(mypart[cmdlen:], mypos - cmdlen) diff --git a/alot/db/manager.py b/alot/db/manager.py index 53170f87..0ea9e7db 100644 --- a/alot/db/manager.py +++ b/alot/db/manager.py @@ -152,6 +152,11 @@ class DBManager(object): path = current_item[2] db.remove_message(path) + elif cmd == 'setconfig': + key = current_item[2] + value = current_item[3] + db.set_config(key, value) + else: # tag/set/untag querystring, tags = current_item[2:] query = db.create_query(querystring) @@ -298,6 +303,14 @@ class DBManager(object): db = Database(path=self.path) return [t for t in db.get_all_tags()] + def get_named_queries(self): + """ + returns the named queries stored in the database. + :rtype: dict (str -> str) mapping alias to full query string + """ + db = Database(path=self.path) + return {k[6:]: v for k, v in db.get_configs('query.')} + def async(self, cbl, fun): """ return a pair (pipe, process) so that the process writes @@ -438,3 +451,36 @@ class DBManager(object): raise DatabaseROError() path = message.get_filename() self.writequeue.append(('remove', afterwards, path)) + + def save_named_query(self, alias, querystring, afterwards=None): + """ + add an alias for a query string. + + These are stored in the notmuch database and can be used as part of + more complex queries using the syntax "query:alias". + See :manpage:`notmuch-search-terms(7)` for more info. + + :param alias: name of shortcut + :type alias: str + :param querystring: value, i.e., the full query string + :type querystring: str + :param afterwards: callback to trigger after adding the alias + :type afterwards: callable or None + """ + if self.ro: + raise DatabaseROError() + self.writequeue.append(('setconfig', afterwards, 'query.' + alias, + querystring)) + + def remove_named_query(self, alias, afterwards=None): + """ + remove a named query from the notmuch database. + + :param alias: name of shortcut + :type alias: str + :param afterwards: callback to trigger after adding the alias + :type afterwards: callable or None + """ + if self.ro: + raise DatabaseROError() + self.writequeue.append(('setconfig', afterwards, 'query.' + alias, '')) diff --git a/alot/defaults/alot.rc.spec b/alot/defaults/alot.rc.spec index 75e76841..dfe5dd38 100644 --- a/alot/defaults/alot.rc.spec +++ b/alot/defaults/alot.rc.spec @@ -152,6 +152,12 @@ thread_statusbar = mixed_list(string, string, default=list('[{buffer_no}: thread # that will be substituted accordingly. taglist_statusbar = mixed_list(string, string, default=list('[{buffer_no}: taglist]','{input_queue} total messages: {total_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. +# These strings may contain variables listed at :ref:`bufferlist_statusbar <bufferlist-statusbar>` +# that will be substituted accordingly. +namedqueries_statusbar = mixed_list(string, string, default=list('[{buffer_no}: namedqueries]','{query_count} named queries')) + # Format of the status-bar in envelope 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 <bufferlist-statusbar>` diff --git a/alot/defaults/default.bindings b/alot/defaults/default.bindings index e9b1e02a..24c90043 100644 --- a/alot/defaults/default.bindings +++ b/alot/defaults/default.bindings @@ -58,6 +58,9 @@ q = exit [taglist] enter = select +[namedqueries] + enter = select + [thread] enter = select C = fold * diff --git a/alot/defaults/default.theme b/alot/defaults/default.theme index 473bc2a5..b97dac50 100644 --- a/alot/defaults/default.theme +++ b/alot/defaults/default.theme @@ -24,6 +24,10 @@ line_focus = 'standout','','yellow','light gray','#ff8','g58' line_even = 'default','','light gray','black','default','g3' line_odd = 'default','','light gray','black','default','default' +[namedqueries] + line_focus = 'standout','','yellow','light gray','#ff8','g58' + line_even = 'default','','light gray','black','default','g3' + line_odd = 'default','','light gray','black','default','default' [thread] arrow_heads = '','','dark red','','#a00','' arrow_bars = '','','dark red','','#800','' diff --git a/alot/defaults/theme.spec b/alot/defaults/theme.spec index a316e320..387bbe2a 100644 --- a/alot/defaults/theme.spec +++ b/alot/defaults/theme.spec @@ -22,6 +22,10 @@ line_focus = attrtriple line_even = attrtriple line_odd = attrtriple +[namedqueries] + line_focus = attrtriple + line_even = attrtriple + line_odd = attrtriple [search] [[threadline]] normal = attrtriple diff --git a/alot/widgets/namedqueries.py b/alot/widgets/namedqueries.py new file mode 100644 index 00000000..9791d5a3 --- /dev/null +++ b/alot/widgets/namedqueries.py @@ -0,0 +1,30 @@ +# Copyright (C) 2011-2018 Patrick Totzke <patricktotzke@gmail.com> +# This file is released under the GNU GPL, version 3 or a later revision. +# For further details see the COPYING file + +""" +Widgets specific to Namedqueries mode +""" +import urwid + + +class QuerylineWidget(urwid.Columns): + def __init__(self, key, value, count, count_unread): + self.query = key + + count_widget = urwid.Text('{0:>7} {1:7}'.\ + format(count, '({0})'.format(count_unread))) + key_widget = urwid.Text(key) + value_widget = urwid.Text(value) + + urwid.Columns.__init__(self, (key_widget, count_widget, value_widget), + dividechars=1) + + def selectable(self): + return True + + def keypress(self, size, key): + return key + + def get_query(self): + return self.query |