Repository: qutebrowser/qutebrowser

Issue:
Special keybindings are overridden case-sensitively only
When doing `:bind <ctrl-z> message-info foo`, doing `:bind <ctrl-Z> message-info bar` doesn't override the existing binding, i.e. pressing <kbd>Ctrl+z</kbd> still shows `foo`.

:bind is case-sensitive
```
:bind <ctrl-T> message-info this
:bind <Ctrl-T> message-info shouldn't
:bind <ctrl-t> message-info work
```

Pressing `ctrl-t` still executes the originally mapped command `open -t`, but none of the above complain about a conflict.

I guess for now we should capitalize all keys when we load or set bindings (as `<Ctrl-T>` seems to be the canonical way to spell it)?



Discussion:
Somewhat similarly, binding `<ctrl+z>` also doesn't override the binding (or only sometimes?)



Resolving patch:
diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py
--- a/qutebrowser/config/parsers/keyconf.py
+++ b/qutebrowser/config/parsers/keyconf.py
@@ -154,7 +154,7 @@ def save(self):
     @cmdutils.argument('win_id', win_id=True)
     @cmdutils.argument('key', completion=usertypes.Completion.empty)
     @cmdutils.argument('command', completion=usertypes.Completion.command)
-    def bind(self, key, win_id, command=None, *, mode=None, force=False):
+    def bind(self, key, win_id, command=None, *, mode='normal', force=False):
         """Bind a key to a command.
 
         Args:
@@ -165,8 +165,9 @@ def bind(self, key, win_id, command=None, *, mode=None, force=False):
                   (default: `normal`).
             force: Rebind the key if it is already bound.
         """
-        if mode is None:
-            mode = 'normal'
+        if utils.is_special_key(key):
+            # <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
+            key = key.lower()
 
         if command is None:
             cmd = self.get_bindings_for(mode).get(key, None)
@@ -198,7 +199,7 @@ def bind(self, key, win_id, command=None, *, mode=None, force=False):
             self._mark_config_dirty()
 
     @cmdutils.register(instance='key-config')
-    def unbind(self, key, mode=None):
+    def unbind(self, key, mode='normal'):
         """Unbind a keychain.
 
         Args:
@@ -206,8 +207,10 @@ def unbind(self, key, mode=None):
             mode: A comma-separated list of modes to unbind the key in
                   (default: `normal`).
         """
-        if mode is None:
-            mode = 'normal'
+        if utils.is_special_key(key):
+            # <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
+            key = key.lower()
+
         mode = self._normalize_sectname(mode)
         for m in mode.split(','):
             if m not in configdata.KEY_DATA:
@@ -377,6 +380,9 @@ def _read_keybinding(self, line):
 
     def _add_binding(self, sectname, keychain, command, *, force=False):
         """Add a new binding from keychain to command in section sectname."""
+        if utils.is_special_key(keychain):
+            # <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
+            keychain = keychain.lower()
         log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
             keychain, command, sectname))
         if sectname not in self.keybindings:


Test patch:
diff --git a/tests/end2end/features/keyinput.feature b/tests/end2end/features/keyinput.feature
--- a/tests/end2end/features/keyinput.feature
+++ b/tests/end2end/features/keyinput.feature
@@ -45,6 +45,22 @@ Feature: Keyboard input
         And I run :bind --mode=caret test08
         Then the message "test08 is bound to 'message-info bar' in caret mode" should be shown
 
+    Scenario: Binding special keys with differing case (issue 1544)
+        When I run :bind <ctrl-test21> message-info test01
+        And I run :bind <Ctrl-Test21> message-info test01
+        Then the error "Duplicate keychain <ctrl-test21> - use --force to override!" should be shown
+
+    Scenario: Print a special binding with differing case (issue 1544)
+        When I run :bind <Ctrl-Test22> message-info foo
+        And I run :bind <ctrl-test22>
+        Then the message "<ctrl-test22> is bound to 'message-info foo' in normal mode" should be shown
+
+    Scenario: Overriding a special binding with differing case (issue 816)
+        When I run :bind <ctrl-test23> message-info foo
+        And I run :bind --force <Ctrl-Test23> message-info bar
+        And I run :bind <ctrl-test23>
+        Then the message "<ctrl-test23> is bound to 'message-info bar' in normal mode" should be shown
+
     # :unbind
 
     Scenario: Binding and unbinding a keychain
@@ -67,6 +83,12 @@ Feature: Keyboard input
         Then "No binding found for o." should be logged
         # maybe check it's unbound in the config?
 
+    Scenario: Binding and unbinding a special keychain with differing case (issue 1544)
+        When I run :bind <ctrl-test24> message-error test09
+        And I run :unbind <Ctrl-Test24>
+        When I run :bind <ctrl-test24>
+        Then the message "<ctrl-test24> is unbound in normal mode" should be shown
+
     # :clear-keychain
 
     Scenario: Clearing the keychain