summaryrefslogtreecommitdiff
path: root/alot/completion
diff options
context:
space:
mode:
authorPatrick Totzke <patricktotzke@gmail.com>2019-08-17 10:00:30 +0100
committerPatrick Totzke <patricktotzke@gmail.com>2019-08-17 11:10:37 +0100
commitd8d1429ec3daf8f2a67ffccbc2aa1d54eb3639c6 (patch)
tree3f13fd449bacd97432e18ba4fb0fb4b19ac0b5ed /alot/completion
parentb5e612c69b625271424b626da24d941ddbe39391 (diff)
refactor prompt completion
This just splits the file completion.py into several files, one for each Completer subclass.
Diffstat (limited to 'alot/completion')
-rw-r--r--alot/completion/__init__.py17
-rw-r--r--alot/completion/abooks.py42
-rw-r--r--alot/completion/accounts.py19
-rw-r--r--alot/completion/argparse.py41
-rw-r--r--alot/completion/command.py221
-rw-r--r--alot/completion/commandline.py54
-rw-r--r--alot/completion/commandname.py26
-rw-r--r--alot/completion/completer.py37
-rw-r--r--alot/completion/contacts.py21
-rw-r--r--alot/completion/cryptokey.py24
-rw-r--r--alot/completion/multipleselection.py48
-rw-r--r--alot/completion/namedquery.py18
-rw-r--r--alot/completion/path.py43
-rw-r--r--alot/completion/query.py57
-rw-r--r--alot/completion/stringlist.py32
-rw-r--r--alot/completion/tag.py17
-rw-r--r--alot/completion/tags.py18
17 files changed, 735 insertions, 0 deletions
diff --git a/alot/completion/__init__.py b/alot/completion/__init__.py
new file mode 100644
index 00000000..86e5d93c
--- /dev/null
+++ b/alot/completion/__init__.py
@@ -0,0 +1,17 @@
+from .completer import Completer
+
+from .abooks import AbooksCompleter
+from .accounts import AccountCompleter
+from .argparse import ArgparseOptionCompleter
+from .command import CommandCompleter
+from .commandline import CommandLineCompleter
+from .commandname import CommandNameCompleter
+from .contacts import ContactsCompleter
+from .cryptokey import CryptoKeyCompleter
+from .multipleselection import MultipleSelectionCompleter
+from .namedquery import NamedQueryCompleter
+from .path import PathCompleter
+from .query import QueryCompleter
+from .stringlist import StringlistCompleter
+from .tag import TagCompleter
+from .tags import TagsCompleter
diff --git a/alot/completion/abooks.py b/alot/completion/abooks.py
new file mode 100644
index 00000000..bc2fa20b
--- /dev/null
+++ b/alot/completion/abooks.py
@@ -0,0 +1,42 @@
+# Copyright (C) 2011-2019 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
+
+from .completer import Completer
+from ..addressbook import AddressbookError
+from ..db.utils import formataddr
+from ..errors import CompletionError
+
+
+class AbooksCompleter(Completer):
+ """Complete a contact from given address books."""
+
+ def __init__(self, abooks, addressesonly=False):
+ """
+ :param abooks: used to look up email addresses
+ :type abooks: list of :class:`~alot.account.AddresBook`
+ :param addressesonly: only insert address, not the realname of the
+ contact
+ :type addressesonly: bool
+ """
+ self.abooks = abooks
+ self.addressesonly = addressesonly
+
+ def complete(self, original, pos):
+ if not self.abooks:
+ return []
+ prefix = original[:pos]
+ res = []
+ for abook in self.abooks:
+ try:
+ res = res + abook.lookup(prefix)
+ except AddressbookError as e:
+ raise CompletionError(e)
+ if self.addressesonly:
+ returnlist = [(addr, len(addr)) for (name, addr) in res]
+ else:
+ returnlist = []
+ for name, addr in res:
+ newtext = formataddr((name, addr))
+ returnlist.append((newtext, len(newtext)))
+ return returnlist
diff --git a/alot/completion/accounts.py b/alot/completion/accounts.py
new file mode 100644
index 00000000..c83ce09c
--- /dev/null
+++ b/alot/completion/accounts.py
@@ -0,0 +1,19 @@
+# Copyright (C) 2011-2019 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 email.utils
+
+from .stringlist import StringlistCompleter
+from ..settings.const import settings
+
+
+class AccountCompleter(StringlistCompleter):
+ """Completes users' own mailaddresses."""
+
+ def __init__(self, **kwargs):
+ accounts = settings.get_accounts()
+ resultlist = [email.utils.formataddr((a.realname, str(a.address)))
+ for a in accounts]
+ StringlistCompleter.__init__(self, resultlist, match_anywhere=True,
+ **kwargs)
diff --git a/alot/completion/argparse.py b/alot/completion/argparse.py
new file mode 100644
index 00000000..ac15b2c6
--- /dev/null
+++ b/alot/completion/argparse.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2011-2019 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 .completer import Completer
+from ..utils import argparse as cargparse
+
+
+class ArgparseOptionCompleter(Completer):
+ """completes option parameters for a given argparse.Parser"""
+ def __init__(self, parser):
+ """
+ :param parser: the option parser we look up parameter and choices from
+ :type parser: `argparse.ArgumentParser`
+ """
+ self.parser = parser
+ self.actions = parser._optionals._actions
+
+ def complete(self, original, pos):
+ pref = original[:pos]
+
+ res = []
+ for act in self.actions:
+ if '=' in pref:
+ optionstring = pref[:pref.rfind('=') + 1]
+ # get choices
+ if 'choices' in act.__dict__:
+ # TODO: respect prefix
+ choices = act.choices or []
+ res = res + [optionstring + a for a in choices]
+ else:
+ for optionstring in act.option_strings:
+ if optionstring.startswith(pref):
+ # append '=' for options that await a string value
+ if isinstance(act, (argparse._StoreAction,
+ cargparse.BooleanAction)):
+ optionstring += '='
+ res.append(optionstring)
+
+ return [(a, len(a)) for a in res]
diff --git a/alot/completion/command.py b/alot/completion/command.py
new file mode 100644
index 00000000..305e445f
--- /dev/null
+++ b/alot/completion/command.py
@@ -0,0 +1,221 @@
+# Copyright (C) 2011-2019 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 logging
+
+from alot import commands
+from alot.buffers import EnvelopeBuffer
+from alot.settings.const import settings
+from alot.utils.cached_property import cached_property
+from .completer import Completer
+from .commandname import CommandNameCompleter
+from .tag import TagCompleter
+from .query import QueryCompleter
+from .contacts import ContactsCompleter
+from .accounts import AccountCompleter
+from .path import PathCompleter
+from .stringlist import StringlistCompleter
+from .multipleselection import MultipleSelectionCompleter
+from .cryptokey import CryptoKeyCompleter
+from .argparse import ArgparseOptionCompleter
+
+
+class CommandCompleter(Completer):
+ """completes one command consisting of command name and parameters"""
+
+ def __init__(self, dbman, mode, currentbuffer=None):
+ """
+ :param dbman: used to look up available tagstrings
+ :type dbman: :class:`~alot.db.DBManager`
+ :param mode: mode identifier
+ :type mode: str
+ :param currentbuffer: currently active buffer. If defined, this will be
+ used to dynamically extract possible completion
+ strings
+ :type currentbuffer: :class:`~alot.buffers.Buffer`
+ """
+ self.dbman = dbman
+ self.mode = mode
+ self.currentbuffer = currentbuffer
+ self._commandnamecompleter = CommandNameCompleter(mode)
+
+ @cached_property
+ def _querycompleter(self):
+ return QueryCompleter(self.dbman)
+
+ @cached_property
+ def _tagcompleter(self):
+ return TagCompleter(self.dbman)
+
+ @cached_property
+ def _contactscompleter(self):
+ abooks = settings.get_addressbooks()
+ return ContactsCompleter(abooks)
+
+ @cached_property
+ def _pathcompleter(self):
+ return PathCompleter()
+
+ @cached_property
+ def _accountscompleter(self):
+ return AccountCompleter()
+
+ @cached_property
+ def _secretkeyscompleter(self):
+ return CryptoKeyCompleter(private=True)
+
+ @cached_property
+ def _publickeyscompleter(self):
+ return CryptoKeyCompleter(private=False)
+
+ def complete(self, line, pos):
+ # remember how many preceding space characters we see until the command
+ # string starts. We'll continue to complete from there on and will add
+ # these whitespaces again at the very end
+ whitespaceoffset = len(line) - len(line.lstrip())
+ line = line[whitespaceoffset:]
+ pos = pos - whitespaceoffset
+
+ words = line.split(' ', 1)
+
+ res = []
+ if pos <= len(words[0]): # we complete commands
+ for cmd, cpos in self._commandnamecompleter.complete(line, pos):
+ newtext = ('%s %s' % (cmd, ' '.join(words[1:])))
+ res.append((newtext, cpos + 1))
+ else:
+ cmd, params = words
+ localpos = pos - (len(cmd) + 1)
+ parser = commands.lookup_parser(cmd, self.mode)
+ if parser is not None:
+ # set 'res' - the result set of matching completionstrings
+ # depending on the current mode and command
+
+ # detect if we are completing optional parameter
+ arguments_until_now = params[:localpos].split(' ')
+ all_optionals = True
+ logging.debug(str(arguments_until_now))
+ for a in arguments_until_now:
+ logging.debug(a)
+ if a and not a.startswith('-'):
+ all_optionals = False
+ # complete optional parameter if
+ # 1. all arguments prior to current position are optional
+ # 2. the parameter starts with '-' or we are at its beginning
+ if all_optionals:
+ myarg = arguments_until_now[-1]
+ start_myarg = params.rindex(myarg)
+ beforeme = params[:start_myarg]
+ # set up local stringlist completer
+ # and let it complete for given list of options
+ localcompleter = ArgparseOptionCompleter(parser)
+ localres = localcompleter.complete(myarg, len(myarg))
+ res = [(
+ beforeme + c, p + start_myarg) for (c, p) in localres]
+
+ # global
+ elif cmd == 'search':
+ res = self._querycompleter.complete(params, localpos)
+ elif cmd == 'help':
+ res = self._commandnamecompleter.complete(params, localpos)
+ elif cmd in ['compose']:
+ res = self._contactscompleter.complete(params, localpos)
+ # search
+ elif self.mode == 'search' and cmd == 'refine':
+ res = self._querycompleter.complete(params, localpos)
+ elif self.mode == 'search' and cmd in ['tag', 'retag', 'untag',
+ 'toggletags']:
+ localcomp = MultipleSelectionCompleter(self._tagcompleter,
+ separator=',')
+ res = localcomp.complete(params, localpos)
+ elif self.mode == 'search' and cmd == 'toggletag':
+ localcomp = MultipleSelectionCompleter(self._tagcompleter,
+ separator=' ')
+ res = localcomp.complete(params, localpos)
+ # envelope
+ elif self.mode == 'envelope' and cmd == 'set':
+ plist = params.split(' ', 1)
+ if len(plist) == 1: # complete from header keys
+ localprefix = params
+ headers = ['Subject', 'To', 'Cc', 'Bcc', 'In-Reply-To',
+ 'From']
+ localcompleter = StringlistCompleter(headers)
+ localres = localcompleter.complete(
+ localprefix, localpos)
+ res = [(c, p + 6) for (c, p) in localres]
+ else: # must have 2 elements
+ header, params = plist
+ localpos = localpos - (len(header) + 1)
+ if header.lower() in ['to', 'cc', 'bcc']:
+ res = self._contactscompleter.complete(params,
+ localpos)
+ elif header.lower() == 'from':
+ res = self._accountscompleter.complete(params,
+ localpos)
+
+ # prepend 'set ' + header and correct position
+ def f(completed, pos):
+ return ('%s %s' % (header, completed),
+ pos + len(header) + 1)
+ res = [f(c, p) for c, p in res]
+ logging.debug(res)
+
+ elif self.mode == 'envelope' and cmd == 'unset':
+ plist = params.split(' ', 1)
+ if len(plist) == 1: # complete from header keys
+ localprefix = params
+ buf = self.currentbuffer
+ if buf:
+ if isinstance(buf, EnvelopeBuffer):
+ available = buf.envelope.headers.keys()
+ localcompleter = StringlistCompleter(available)
+ localres = localcompleter.complete(localprefix,
+ localpos)
+ res = [(c, p + 6) for (c, p) in localres]
+
+ elif self.mode == 'envelope' and cmd == 'attach':
+ res = self._pathcompleter.complete(params, localpos)
+ elif self.mode == 'envelope' and cmd in ['sign', 'togglesign']:
+ res = self._secretkeyscompleter.complete(params, localpos)
+ elif self.mode == 'envelope' and cmd in ['encrypt',
+ 'rmencrypt',
+ 'toggleencrypt']:
+ res = self._publickeyscompleter.complete(params, localpos)
+ elif self.mode == 'envelope' and cmd in ['tag', 'toggletags',
+ 'untag', 'retag']:
+ localcomp = MultipleSelectionCompleter(self._tagcompleter,
+ separator=',')
+ res = localcomp.complete(params, localpos)
+ # thread
+ elif self.mode == 'thread' and cmd == 'save':
+ res = self._pathcompleter.complete(params, localpos)
+ elif self.mode == 'thread' and cmd in ['fold', 'unfold',
+ 'togglesource',
+ 'toggleheaders']:
+ res = self._querycompleter.complete(params, localpos)
+ elif self.mode == 'thread' and cmd in ['tag', 'retag', 'untag',
+ 'toggletags']:
+ localcomp = MultipleSelectionCompleter(self._tagcompleter,
+ separator=',')
+ res = localcomp.complete(params, localpos)
+ elif cmd == 'move':
+ directions = ['up', 'down', 'page up', 'page down',
+ 'halfpage up', 'halfpage down', 'first',
+ 'last']
+ if self.mode == 'thread':
+ directions += ['parent', 'first reply', 'last reply',
+ 'next sibling', 'previous sibling',
+ 'next', 'previous', 'next unfolded',
+ 'previous unfolded']
+ localcompleter = StringlistCompleter(directions)
+ res = localcompleter.complete(params, localpos)
+
+ # prepend cmd and correct position
+ res = [('%s %s' % (cmd, t), p + len(cmd) + 1)
+ for (t, p) in res]
+
+ # re-insert whitespaces and correct position
+ wso = whitespaceoffset
+ res = [(' ' * wso + cmdstr, p + wso) for cmdstr, p in res]
+ return res
diff --git a/alot/completion/commandline.py b/alot/completion/commandline.py
new file mode 100644
index 00000000..f5739f38
--- /dev/null
+++ b/alot/completion/commandline.py
@@ -0,0 +1,54 @@
+# Copyright (C) 2011-2019 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
+
+from .completer import Completer
+from .command import CommandCompleter
+from ..helper import split_commandline
+
+
+class CommandLineCompleter(Completer):
+ """completes command lines: semicolon separated command strings"""
+
+ def __init__(self, dbman, mode, currentbuffer=None):
+ """
+ :param dbman: used to look up available tagstrings
+ :type dbman: :class:`~alot.db.DBManager`
+ :param mode: mode identifier
+ :type mode: str
+ :param currentbuffer: currently active buffer. If defined, this will be
+ used to dynamically extract possible completion
+ strings
+ :type currentbuffer: :class:`~alot.buffers.Buffer`
+ """
+ self._commandcompleter = CommandCompleter(dbman, mode, currentbuffer)
+
+ @staticmethod
+ def get_context(line, pos):
+ """
+ computes start and end position of substring of line that is the
+ command string under given position
+ """
+ commands = split_commandline(line) + ['']
+ i = 0
+ start = 0
+ end = len(commands[i])
+ while pos > end:
+ i += 1
+ start = end + 1
+ end += 1 + len(commands[i])
+ return start, end
+
+ def complete(self, line, pos):
+ cstart, cend = self.get_context(line, pos)
+ before = line[:cstart]
+ after = line[cend:]
+ cmdstring = line[cstart:cend]
+ cpos = pos - cstart
+
+ res = []
+ for ccmd, ccpos in self._commandcompleter.complete(cmdstring, cpos):
+ newtext = before + ccmd + after
+ newpos = pos + (ccpos - cpos)
+ res.append((newtext, newpos))
+ return res
diff --git a/alot/completion/commandname.py b/alot/completion/commandname.py
new file mode 100644
index 00000000..5c946eb6
--- /dev/null
+++ b/alot/completion/commandname.py
@@ -0,0 +1,26 @@
+# Copyright (C) 2011-2019 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 logging
+from alot import commands
+from .completer import Completer
+
+
+class CommandNameCompleter(Completer):
+ """Completes command names."""
+
+ def __init__(self, mode):
+ """
+ :param mode: mode identifier
+ :type mode: str
+ """
+ self.mode = mode
+
+ def complete(self, original, pos):
+ commandprefix = original[:pos]
+ logging.debug('original="%s" prefix="%s"', original, commandprefix)
+ cmdlist = commands.COMMANDS['global'].copy()
+ cmdlist.update(commands.COMMANDS[self.mode])
+ matching = [t for t in cmdlist if t.startswith(commandprefix)]
+ return [(t, len(t)) for t in matching]
diff --git a/alot/completion/completer.py b/alot/completion/completer.py
new file mode 100644
index 00000000..de15d312
--- /dev/null
+++ b/alot/completion/completer.py
@@ -0,0 +1,37 @@
+# Copyright (C) 2011-2019 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 abc
+
+
+class Completer:
+ """base class for completers"""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def complete(self, original, pos):
+ """returns a list of completions and cursor positions for the
+ string original from position pos on.
+
+ :param original: the string to complete
+ :type original: str
+ :param pos: starting position to complete from
+ :type pos: int
+ :returns: pairs of completed string and cursor position in the
+ new string
+ :rtype: list of (str, int)
+ :raises: :exc:`CompletionError`
+ """
+ pass
+
+ def relevant_part(self, original, pos, sep=' '):
+ """
+ calculates the subword in a `sep`-splitted list of substrings of
+ `original` that `pos` is ia.n
+ """
+ start = original.rfind(sep, 0, pos) + 1
+ end = original.find(sep, pos - 1)
+ if end == -1:
+ end = len(original)
+ return original[start:end], start, end, pos - start
diff --git a/alot/completion/contacts.py b/alot/completion/contacts.py
new file mode 100644
index 00000000..ae4e71cd
--- /dev/null
+++ b/alot/completion/contacts.py
@@ -0,0 +1,21 @@
+# Copyright (C) 2011-2019 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
+
+from .multipleselection import MultipleSelectionCompleter
+from .abooks import AbooksCompleter
+
+
+class ContactsCompleter(MultipleSelectionCompleter):
+ """completes contacts from given address books"""
+
+ def __init__(self, abooks, addressesonly=False):
+ """
+ :param abooks: used to look up email addresses
+ :type abooks: list of :class:`~alot.account.AddresBook`
+ :param addressesonly: only insert address, not the realname of the
+ contact
+ :type addressesonly: bool
+ """
+ self._completer = AbooksCompleter(abooks, addressesonly=addressesonly)
+ self._separator = ', '
diff --git a/alot/completion/cryptokey.py b/alot/completion/cryptokey.py
new file mode 100644
index 00000000..0631eee3
--- /dev/null
+++ b/alot/completion/cryptokey.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2011-2019 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
+
+from alot import crypto
+from .stringlist import StringlistCompleter
+
+
+class CryptoKeyCompleter(StringlistCompleter):
+ """completion for gpg keys"""
+
+ def __init__(self, private=False):
+ """
+ :param private: return private keys
+ :type private: bool
+ """
+ keys = crypto.list_keys(private=private)
+ resultlist = []
+ for k in keys:
+ for s in k.subkeys:
+ resultlist.append(s.keyid)
+ for u in k.uids:
+ resultlist.append(u.email)
+ StringlistCompleter.__init__(self, resultlist, match_anywhere=True)
diff --git a/alot/completion/multipleselection.py b/alot/completion/multipleselection.py
new file mode 100644
index 00000000..45de5a28
--- /dev/null
+++ b/alot/completion/multipleselection.py
@@ -0,0 +1,48 @@
+# Copyright (C) 2011-2019 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
+
+
+from .completer import Completer
+
+
+class MultipleSelectionCompleter(Completer):
+ """
+ Meta-Completer that turns any Completer into one that deals with a list of
+ completion strings using the wrapped Completer.
+ This allows for example to easily construct a completer for comma separated
+ recipient-lists using a :class:`ContactsCompleter`.
+ """
+
+ def __init__(self, completer, separator=', '):
+ """
+ :param completer: completer to use for individual substrings
+ :type completer: Completer
+ :param separator: separator used to split the completion string into
+ substrings to be fed to `completer`.
+ :type separator: str
+ """
+ self._completer = completer
+ self._separator = separator
+
+ def relevant_part(self, original, pos):
+ """Calculate the subword of `original` that `pos` is in."""
+ start = original.rfind(self._separator, 0, pos)
+ if start == -1:
+ start = 0
+ else:
+ start = start + len(self._separator)
+ end = original.find(self._separator, pos - 1)
+ if end == -1:
+ end = len(original)
+ return original[start:end], start, end, pos - start
+
+ def complete(self, original, pos):
+ mypart, start, end, mypos = self.relevant_part(original, pos)
+ res = []
+ for c, _ in self._completer.complete(mypart, mypos):
+ newprefix = original[:start] + c
+ if not original[end:].startswith(self._separator):
+ newprefix += self._separator
+ res.append((newprefix + original[end:], len(newprefix)))
+ return res
diff --git a/alot/completion/namedquery.py b/alot/completion/namedquery.py
new file mode 100644
index 00000000..bcad5d2c
--- /dev/null
+++ b/alot/completion/namedquery.py
@@ -0,0 +1,18 @@
+# Copyright (C) 2011-2019 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
+
+from .stringlist import StringlistCompleter
+
+
+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))
diff --git a/alot/completion/path.py b/alot/completion/path.py
new file mode 100644
index 00000000..c05f42df
--- /dev/null
+++ b/alot/completion/path.py
@@ -0,0 +1,43 @@
+# Copyright (C) 2011-2019 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 glob
+import os
+from .completer import Completer
+
+
+class PathCompleter(Completer):
+ """Completes for file system paths."""
+
+ def complete(self, original, pos):
+ if not original:
+ return [('~/', 2)]
+ prefix = os.path.expanduser(original[:pos])
+
+ def escape(path):
+ """Escape all backslashes and spaces in path with a backslash.
+
+ :param path: the path to escape
+ :type path: str
+ :returns: the escaped path
+ :rtype: str
+ """
+ return path.replace('\\', '\\\\').replace(' ', r'\ ')
+
+ def deescape(escaped_path):
+ """Remove escaping backslashes in front of spaces and backslashes.
+
+ :param escaped_path: a path potentially with escaped spaces and
+ backslashs
+ :type escaped_path: str
+ :returns: the actual path
+ :rtype: str
+ """
+ return escaped_path.replace('\\ ', ' ').replace('\\\\', '\\')
+
+ def prep(path):
+ escaped_path = escape(path)
+ return escaped_path, len(escaped_path)
+
+ return [prep(g) for g in glob.glob(deescape(prefix) + '*')]
diff --git a/alot/completion/query.py b/alot/completion/query.py
new file mode 100644
index 00000000..42ea64a0
--- /dev/null
+++ b/alot/completion/query.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2011-2019 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 re
+
+from alot.settings.const import settings
+from .completer import Completer
+from .abooks import AbooksCompleter
+from .tag import TagCompleter
+from .namedquery import NamedQueryCompleter
+
+
+class QueryCompleter(Completer):
+ """completion for a notmuch query string"""
+ def __init__(self, dbman):
+ """
+ :param dbman: used to look up available tagstrings
+ :type dbman: :class:`~alot.db.DBManager`
+ """
+ self.dbman = dbman
+ 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', '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|query):(\w*)', myprefix)
+ if m:
+ cmd, _ = m.groups()
+ 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)
+ resultlist = []
+ for ltxt, lpos in localres:
+ newtext = original[:start] + cmd + ':' + ltxt + original[end:]
+ newpos = start + len(cmd) + 1 + lpos
+ resultlist.append((newtext, newpos))
+ return resultlist
+ else:
+ matched = (t for t in self.keywords if t.startswith(myprefix))
+ resultlist = []
+ for keyword in matched:
+ newprefix = original[:start] + keyword + ':'
+ resultlist.append((newprefix + original[end:], len(newprefix)))
+ return resultlist
diff --git a/alot/completion/stringlist.py b/alot/completion/stringlist.py
new file mode 100644
index 00000000..9d7fa3d3
--- /dev/null
+++ b/alot/completion/stringlist.py
@@ -0,0 +1,32 @@
+# Copyright (C) 2011-2019 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 re
+
+from .completer import Completer
+
+
+class StringlistCompleter(Completer):
+ """Completer for a fixed list of strings."""
+
+ def __init__(self, resultlist, ignorecase=True, match_anywhere=False):
+ """
+ :param resultlist: strings used for completion
+ :type resultlist: list of str
+ :param liberal: match case insensitive and not prefix-only
+ :type liberal: bool
+ """
+ self.resultlist = resultlist
+ self.flags = re.IGNORECASE if ignorecase else 0
+ self.match_anywhere = match_anywhere
+
+ def complete(self, original, pos):
+ pref = original[:pos]
+
+ re_prefix = '.*' if self.match_anywhere else ''
+
+ def match(s, m):
+ r = '{}{}.*'.format(re_prefix, re.escape(m))
+ return re.match(r, s, flags=self.flags) is not None
+
+ return [(a, len(a)) for a in self.resultlist if match(a, pref)]
diff --git a/alot/completion/tag.py b/alot/completion/tag.py
new file mode 100644
index 00000000..fd6ed119
--- /dev/null
+++ b/alot/completion/tag.py
@@ -0,0 +1,17 @@
+# Copyright (C) 2011-2019 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
+
+from .stringlist import StringlistCompleter
+
+
+class TagCompleter(StringlistCompleter):
+ """Complete a tagstring."""
+
+ def __init__(self, dbman):
+ """
+ :param dbman: used to look up available tagstrings
+ :type dbman: :class:`~alot.db.DBManager`
+ """
+ resultlist = dbman.get_all_tags()
+ StringlistCompleter.__init__(self, resultlist)
diff --git a/alot/completion/tags.py b/alot/completion/tags.py
new file mode 100644
index 00000000..b151e6f5
--- /dev/null
+++ b/alot/completion/tags.py
@@ -0,0 +1,18 @@
+# Copyright (C) 2011-2019 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
+
+from .multipleselection import MultipleSelectionCompleter
+from .tag import TagCompleter
+
+
+class TagsCompleter(MultipleSelectionCompleter):
+ """Complete a comma separated list of tagstrings."""
+
+ def __init__(self, dbman):
+ """
+ :param dbman: used to look up available tagstrings
+ :type dbman: :class:`~alot.db.DBManager`
+ """
+ self._completer = TagCompleter(dbman)
+ self._separator = ','