diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py
index 831243312..4b48290d9 100644
--- a/qutebrowser/completion/models/miscmodels.py
+++ b/qutebrowser/completion/models/miscmodels.py
@@ -24,7 +24,7 @@ from PyQt5.QtCore import Qt, QTimer, pyqtSlot
from qutebrowser.browser import webview
from qutebrowser.config import config, configdata
-from qutebrowser.utils import objreg, log, qtutils
+from qutebrowser.utils import objreg, log, qtutils, utils
from qutebrowser.commands import cmdutils
from qutebrowser.completion.models import base
@@ -57,7 +57,7 @@ class CommandCompletionModel(base.BaseCompletionModel):
cmd_to_keys = defaultdict(list)
for key, cmd in keyconf.get_bindings_for('normal').items():
# put special bindings last
- if key.startswith('<') and key.endswith('>'):
+ if utils.is_special_key(key):
cmd_to_keys[cmd].append(key)
else:
cmd_to_keys[cmd].insert(0, key)
diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py
index 4accafcb6..dd8535234 100644
--- a/qutebrowser/keyinput/basekeyparser.py
+++ b/qutebrowser/keyinput/basekeyparser.py
@@ -335,7 +335,7 @@ class BaseKeyParser(QObject):
def _parse_key_command(self, modename, key, cmd):
"""Parse the keys and their command and store them in the object."""
- if key.startswith('<') and key.endswith('>'):
+ if utils.is_special_key(key):
keystr = utils.normalize_keystr(key[1:-1])
self.special_bindings[keystr] = cmd
elif self._supports_chains:
diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py
index 9a4d99d95..c3864c691 100644
--- a/qutebrowser/misc/keyhintwidget.py
+++ b/qutebrowser/misc/keyhintwidget.py
@@ -94,28 +94,31 @@ class KeyHintView(QLabel):
self.hide()
return
+ keyconf = objreg.get('key-config')
+ bindings = [(k, v) for (k, v)
+ in keyconf.get_bindings_for(modename).items()
+ if k.startswith(prefix) and not utils.is_special_key(k)]
+
+ if not bindings:
+ return
+
self.show()
suffix_color = html.escape(config.get('colors', 'keyhint.fg.suffix'))
text = ''
- keyconf = objreg.get('key-config')
- # this is only fired in normal mode
- for key, cmd in keyconf.get_bindings_for(modename).items():
- # for now, special keys can't be part of keychains, so ignore them
- is_special_binding = key.startswith('<') and key.endswith('>')
- if key.startswith(prefix) and not is_special_binding:
- text += (
- "
"
- "{} | "
- "{} | "
- "{} | "
- "
"
- ).format(
- html.escape(prefix),
- suffix_color,
- html.escape(key[len(prefix):]),
- html.escape(cmd)
- )
+ for key, cmd in bindings:
+ text += (
+ ""
+ "{} | "
+ "{} | "
+ "{} | "
+ "
"
+ ).format(
+ html.escape(prefix),
+ suffix_color,
+ html.escape(key[len(prefix):]),
+ html.escape(cmd)
+ )
text = ''.format(text)
self.setText(text)
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index c04516d36..febceca76 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -440,9 +440,14 @@ class KeyParseError(Exception):
super().__init__("Could not parse {!r}: {}".format(keystr, error))
+def is_special_key(keystr):
+ """True if keystr is a 'special' keystring (e.g. or )."""
+ return keystr.startswith('<') and keystr.endswith('>')
+
+
def _parse_single_key(keystr):
"""Convert a single key string to a (Qt.Key, Qt.Modifiers, text) tuple."""
- if keystr.startswith('<') and keystr.endswith('>'):
+ if is_special_key(keystr):
# Special key
keystr = keystr[1:-1]
elif len(keystr) == 1:
@@ -489,7 +494,7 @@ def _parse_single_key(keystr):
def parse_keystring(keystr):
"""Parse a keystring like or xyz and return a KeyInfo list."""
- if keystr.startswith('<') and keystr.endswith('>'):
+ if is_special_key(keystr):
return [_parse_single_key(keystr)]
else:
return [_parse_single_key(char) for char in keystr]
diff --git a/tests/unit/misc/test_keyhints.py b/tests/unit/misc/test_keyhints.py
index 85f6c55ce..2f312e8b3 100644
--- a/tests/unit/misc/test_keyhints.py
+++ b/tests/unit/misc/test_keyhints.py
@@ -110,3 +110,12 @@ def test_color_switch(keyhint, config_stub, key_config_stub):
('aa', 'cmd-aa')]))
keyhint.update_keyhint('normal', 'a')
assert keyhint.text() == expected_text(('a', '#ABCDEF', 'a', 'cmd-aa'))
+
+
+def test_no_matches(keyhint, key_config_stub):
+ """Ensure the widget isn't visible if there are no keystrings to show."""
+ key_config_stub.set_bindings_for('normal', OrderedDict([
+ ('aa', 'cmd-aa'),
+ ('ab', 'cmd-ab')]))
+ keyhint.update_keyhint('normal', 'z')
+ assert not keyhint.isVisible()
diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py
index b6950e969..52c36eee3 100644
--- a/tests/unit/utils/test_utils.py
+++ b/tests/unit/utils/test_utils.py
@@ -984,3 +984,19 @@ class TestGetSetClipboard:
def test_supports_selection(self, clipboard_mock, selection):
clipboard_mock.supportsSelection.return_value = selection
assert utils.supports_selection() == selection
+
+
+@pytest.mark.parametrize('keystr, expected', [
+ ('', True),
+ ('', True),
+ ('', True),
+ ('x', False),
+ ('X', False),
+ ('', True),
+ ('foobar', False),
+ ('foo>', False),
+ ('