summaryrefslogtreecommitdiff
path: root/alot
diff options
context:
space:
mode:
authorDylan Baker <dylan@pnwbakers.com>2018-07-24 14:41:22 -0700
committerGitHub <noreply@github.com>2018-07-24 14:41:22 -0700
commit4cba47a8143059a68c904e40e575fc4d493bf6de (patch)
tree8800830a4dea35c17b951984b88c39dfafd47a09 /alot
parentbf891cec60ea4b535261d976a124bd5b683ce437 (diff)
parent6ab2f27b2c15aab4f340b7770598235b155a06b7 (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__.py3
-rw-r--r--alot/buffers/__init__.py1
-rw-r--r--alot/buffers/namedqueries.py78
-rw-r--r--alot/commands/__init__.py1
-rw-r--r--alot/commands/globals.py106
-rw-r--r--alot/commands/namedqueries.py30
-rw-r--r--alot/commands/search.py22
-rw-r--r--alot/completion.py23
-rw-r--r--alot/db/manager.py46
-rw-r--r--alot/defaults/alot.rc.spec6
-rw-r--r--alot/defaults/default.bindings3
-rw-r--r--alot/defaults/default.theme4
-rw-r--r--alot/defaults/theme.spec4
-rw-r--r--alot/widgets/namedqueries.py30
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