Initial :bind/:unbind implementation
This commit is contained in:
parent
290d27a064
commit
ba1bc29a97
@ -184,7 +184,7 @@ def _process_args(args):
|
|||||||
config_obj = objreg.get('config')
|
config_obj = objreg.get('config')
|
||||||
for opt, val in args.temp_settings:
|
for opt, val in args.temp_settings:
|
||||||
try:
|
try:
|
||||||
config_obj.set(opt, val)
|
config_obj.set_str(opt, val)
|
||||||
except configexc.Error as e:
|
except configexc.Error as e:
|
||||||
message.error("set: {} - {}".format(e.__class__.__name__, e))
|
message.error("set: {} - {}".format(e.__class__.__name__, e))
|
||||||
|
|
||||||
|
@ -457,7 +457,7 @@ def _qute_settings_set(url):
|
|||||||
return 'text/html', b'error: ' + msg.encode('utf-8')
|
return 'text/html', b'error: ' + msg.encode('utf-8')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config.instance.set(option, value)
|
config.instance.set_str(option, value)
|
||||||
return 'text/html', b'ok'
|
return 'text/html', b'ok'
|
||||||
except configexc.Error as e:
|
except configexc.Error as e:
|
||||||
message.error(str(e))
|
message.error(str(e))
|
||||||
|
@ -27,8 +27,9 @@ import configparser
|
|||||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings
|
from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings
|
||||||
|
|
||||||
from qutebrowser.config import configdata, configexc, configtypes
|
from qutebrowser.config import configdata, configexc, configtypes
|
||||||
from qutebrowser.utils import utils, objreg, message, standarddir, log
|
from qutebrowser.utils import (utils, objreg, message, standarddir, log,
|
||||||
from qutebrowser.commands import cmdexc, cmdutils
|
usertypes)
|
||||||
|
from qutebrowser.commands import cmdexc, cmdutils, runners
|
||||||
|
|
||||||
|
|
||||||
# An easy way to access the config from other code via config.val.foo
|
# An easy way to access the config from other code via config.val.foo
|
||||||
@ -119,10 +120,13 @@ class change_filter: # pylint: disable=invalid-name
|
|||||||
|
|
||||||
class NewKeyConfig:
|
class NewKeyConfig:
|
||||||
|
|
||||||
def get_reverse_bindings_for(self, section):
|
def __init__(self, manager):
|
||||||
"""Get a dict of commands to a list of bindings for the section."""
|
self._manager = manager
|
||||||
|
|
||||||
|
def get_reverse_bindings_for(self, mode):
|
||||||
|
"""Get a dict of commands to a list of bindings for the mode."""
|
||||||
cmd_to_keys = {}
|
cmd_to_keys = {}
|
||||||
bindings = val.bindings.commands[section]
|
bindings = val.bindings.commands[mode]
|
||||||
if bindings is None:
|
if bindings is None:
|
||||||
return cmd_to_keys
|
return cmd_to_keys
|
||||||
for key, full_cmd in bindings.items():
|
for key, full_cmd in bindings.items():
|
||||||
@ -136,6 +140,57 @@ class NewKeyConfig:
|
|||||||
cmd_to_keys[cmd].insert(0, key)
|
cmd_to_keys[cmd].insert(0, key)
|
||||||
return cmd_to_keys
|
return cmd_to_keys
|
||||||
|
|
||||||
|
def _prepare(self, key, mode):
|
||||||
|
"""Make sure the given mode exists and normalize the key."""
|
||||||
|
if mode not in val.bindings.commands:
|
||||||
|
raise configexc.ValidationError(
|
||||||
|
"Invalid mode {} while binding {}!".format(mode, key))
|
||||||
|
if utils.is_special_key(key):
|
||||||
|
# <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
|
||||||
|
return utils.normalize_keystr(key)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def bind(self, key, command, *, mode, force=False):
|
||||||
|
"""Add a new binding from key to command."""
|
||||||
|
key = self._prepare(key, mode)
|
||||||
|
|
||||||
|
parser = runners.CommandParser()
|
||||||
|
try:
|
||||||
|
results = parser.parse_all(command)
|
||||||
|
except cmdexc.Error as e:
|
||||||
|
# FIXME: conf good message?
|
||||||
|
raise configexc.ValidationError("Invalid command: {}".format(e))
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
try:
|
||||||
|
result.cmd.validate_mode(usertypes.KeyMode[mode])
|
||||||
|
except cmdexc.PrerequisitesError as e:
|
||||||
|
# FIXME: conf good message?
|
||||||
|
raise configexc.ValidationError(str(e))
|
||||||
|
|
||||||
|
bindings = val.bindings.commands
|
||||||
|
|
||||||
|
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
|
||||||
|
key, command, mode))
|
||||||
|
if key in bindings[mode] and not force:
|
||||||
|
raise configexc.DuplicateKeyError("Duplicate key {}".format(key))
|
||||||
|
bindings[mode][key] = command
|
||||||
|
val.bindings.commands = bindings # FIXME:conf
|
||||||
|
|
||||||
|
def unbind(self, key, *, mode='normal'):
|
||||||
|
"""Unbind the given key in the given mode."""
|
||||||
|
key = self._prepare(key, mode)
|
||||||
|
try:
|
||||||
|
del val.bindings.commands[mode][key]
|
||||||
|
except KeyError:
|
||||||
|
raise configexc.ValidationError("Unknown binding {}".format(key))
|
||||||
|
val.bindings.commands = val.bindings.commands # FIXME:conf
|
||||||
|
|
||||||
|
def get_command(self, key, mode):
|
||||||
|
"""Get the command for a given key (or None)."""
|
||||||
|
key = self._prepare(key, mode)
|
||||||
|
return val.bindings.commands[mode].get(key, None)
|
||||||
|
|
||||||
|
|
||||||
class ConfigCommands:
|
class ConfigCommands:
|
||||||
|
|
||||||
@ -200,7 +255,7 @@ class ConfigCommands:
|
|||||||
if len(values) == 1:
|
if len(values) == 1:
|
||||||
# If we have only one value, just set it directly (avoid
|
# If we have only one value, just set it directly (avoid
|
||||||
# breaking stuff like aliases or other pseudo-settings)
|
# breaking stuff like aliases or other pseudo-settings)
|
||||||
self._config.set(option, values[0])
|
self._config.set_str(option, values[0])
|
||||||
return
|
return
|
||||||
|
|
||||||
# Use the next valid value from values, or the first if the current
|
# Use the next valid value from values, or the first if the current
|
||||||
@ -212,7 +267,7 @@ class ConfigCommands:
|
|||||||
value = values[idx]
|
value = values[idx]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
value = values[0]
|
value = values[0]
|
||||||
self._config.set(option, value)
|
self._config.set_str(option, value)
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def _handle_config_error(self):
|
def _handle_config_error(self):
|
||||||
@ -225,6 +280,56 @@ class ConfigCommands:
|
|||||||
raise cmdexc.CommandError("set: {} - {}".format(
|
raise cmdexc.CommandError("set: {} - {}".format(
|
||||||
e.__class__.__name__, e))
|
e.__class__.__name__, e))
|
||||||
|
|
||||||
|
@cmdutils.register(instance='config-commands', maxsplit=1,
|
||||||
|
no_cmd_split=True, no_replace_variables=True)
|
||||||
|
@cmdutils.argument('command', completion=usertypes.Completion.bind)
|
||||||
|
def bind(self, key, command=None, *, mode='normal', force=False):
|
||||||
|
"""Bind a key to a command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The keychain or special key (inside `<...>`) to bind.
|
||||||
|
command: The command to execute, with optional args, or None to
|
||||||
|
print the current binding.
|
||||||
|
mode: A comma-separated list of modes to bind the key in
|
||||||
|
(default: `normal`).
|
||||||
|
force: Rebind the key if it is already bound.
|
||||||
|
"""
|
||||||
|
if utils.is_special_key(key):
|
||||||
|
# <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
|
||||||
|
key = utils.normalize_keystr(key)
|
||||||
|
|
||||||
|
if mode not in val.bindings.commands:
|
||||||
|
raise cmdexc.CommandError("Invalid mode {}!".format(mode))
|
||||||
|
|
||||||
|
if command is None:
|
||||||
|
cmd = key_instance.get_command(key, mode)
|
||||||
|
if cmd is None:
|
||||||
|
message.info("{} is unbound in {} mode".format(key, mode))
|
||||||
|
else:
|
||||||
|
message.info("{} is bound to '{}' in {} mode".format(
|
||||||
|
key, cmd, mode))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
key_instance.bind(key, command, mode=mode, force=force)
|
||||||
|
except configexc.DuplicateKeyError as e:
|
||||||
|
raise cmdexc.CommandError(str(e) + " - use --force to override!")
|
||||||
|
except configexc.ValidationError as e:
|
||||||
|
raise cmdexc.CommandError(str(e))
|
||||||
|
|
||||||
|
@cmdutils.register(instance='config-commands')
|
||||||
|
def unbind(self, key, mode='normal'):
|
||||||
|
"""Unbind a keychain.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The keychain or special key (inside <...>) to unbind.
|
||||||
|
mode: A mode to unbind the key in (default: `normal`).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
key_instance.unbind(key, mode=mode)
|
||||||
|
except configexc.ValidationError as e:
|
||||||
|
raise cmdexc.CommandError(str(e))
|
||||||
|
|
||||||
|
|
||||||
class NewConfigManager(QObject):
|
class NewConfigManager(QObject):
|
||||||
|
|
||||||
@ -235,6 +340,10 @@ class NewConfigManager(QObject):
|
|||||||
self.options = {}
|
self.options = {}
|
||||||
self._values = {} # FIXME:conf stub
|
self._values = {} # FIXME:conf stub
|
||||||
|
|
||||||
|
def _changed(self, name, value):
|
||||||
|
self.changed.emit(name)
|
||||||
|
log.config.debug("Config option changed: {} = {}".format(name, value))
|
||||||
|
|
||||||
def read_defaults(self):
|
def read_defaults(self):
|
||||||
for name, option in configdata.DATA.items():
|
for name, option in configdata.DATA.items():
|
||||||
self.options[name] = option
|
self.options[name] = option
|
||||||
@ -256,11 +365,17 @@ class NewConfigManager(QObject):
|
|||||||
return opt.typ.to_str(value)
|
return opt.typ.to_str(value)
|
||||||
|
|
||||||
def set(self, name, value):
|
def set(self, name, value):
|
||||||
|
# FIXME:conf stub
|
||||||
|
opt = self.get_opt(name)
|
||||||
|
opt.typ.to_py(value) # for validation
|
||||||
|
self._values[name] = value
|
||||||
|
self._changed(name, value)
|
||||||
|
|
||||||
|
def set_str(self, name, value):
|
||||||
# FIXME:conf stub
|
# FIXME:conf stub
|
||||||
opt = self.get_opt(name)
|
opt = self.get_opt(name)
|
||||||
self._values[name] = opt.typ.from_str(value)
|
self._values[name] = opt.typ.from_str(value)
|
||||||
self.changed.emit(name)
|
self._changed(name, value)
|
||||||
log.config.debug("Config option changed: {} = {}".format(name, value))
|
|
||||||
|
|
||||||
def dump_userconfig(self):
|
def dump_userconfig(self):
|
||||||
"""Get the part of the config which was changed by the user.
|
"""Get the part of the config which was changed by the user.
|
||||||
@ -301,9 +416,13 @@ class ConfigContainer:
|
|||||||
Those two never overlap as configdata.py ensures there are no shadowing
|
Those two never overlap as configdata.py ensures there are no shadowing
|
||||||
options.
|
options.
|
||||||
"""
|
"""
|
||||||
|
if attr.startswith('_'):
|
||||||
|
return self.__getattribute__(attr)
|
||||||
|
|
||||||
name = self._join(attr)
|
name = self._join(attr)
|
||||||
if configdata.is_valid_prefix(name):
|
if configdata.is_valid_prefix(name):
|
||||||
return ConfigContainer(manager=self._manager, prefix=name)
|
return ConfigContainer(manager=self._manager, prefix=name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self._manager.get(name)
|
return self._manager.get(name)
|
||||||
except configexc.NoOptionError as e:
|
except configexc.NoOptionError as e:
|
||||||
@ -313,7 +432,7 @@ class ConfigContainer:
|
|||||||
def __setattr__(self, attr, value):
|
def __setattr__(self, attr, value):
|
||||||
if attr.startswith('_'):
|
if attr.startswith('_'):
|
||||||
return super().__setattr__(attr, value)
|
return super().__setattr__(attr, value)
|
||||||
self._handler(self._join(attr), value)
|
self._manager.set(self._join(attr), value)
|
||||||
|
|
||||||
def _join(self, attr):
|
def _join(self, attr):
|
||||||
if self._prefix:
|
if self._prefix:
|
||||||
@ -364,7 +483,7 @@ def init(parent=None):
|
|||||||
global val, instance, key_instance
|
global val, instance, key_instance
|
||||||
val = ConfigContainer(new_config)
|
val = ConfigContainer(new_config)
|
||||||
instance = new_config
|
instance = new_config
|
||||||
key_instance = NewKeyConfig()
|
key_instance = NewKeyConfig(new_config)
|
||||||
|
|
||||||
for cf in _change_filters:
|
for cf in _change_filters:
|
||||||
cf.validate()
|
cf.validate()
|
||||||
|
@ -52,6 +52,13 @@ class ValidationError(Error):
|
|||||||
self.option = None
|
self.option = None
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateKeyError(ValidationError):
|
||||||
|
|
||||||
|
"""Raised when there was a duplicate key."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NoOptionError(Error):
|
class NoOptionError(Error):
|
||||||
|
|
||||||
"""Raised when an option was not found."""
|
"""Raised when an option was not found."""
|
||||||
|
@ -729,9 +729,15 @@ class Command(BaseType):
|
|||||||
self._basic_py_validation(value, str)
|
self._basic_py_validation(value, str)
|
||||||
if not value:
|
if not value:
|
||||||
return
|
return
|
||||||
split = value.split()
|
|
||||||
if not split or split[0] not in cmdutils.cmd_dict:
|
# FIXME:conf is it okay to import runners.py here?
|
||||||
raise configexc.ValidationError(value, "must be a valid command!")
|
from qutebrowser.commands import runners, cmdexc
|
||||||
|
parser = runners.CommandParser()
|
||||||
|
try:
|
||||||
|
parser.parse_all(value)
|
||||||
|
except cmdexc.Error as e:
|
||||||
|
raise configexc.ValidationError(value, str(e))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def complete(self):
|
def complete(self):
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
|
||||||
|
|
||||||
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
|
||||||
#
|
|
||||||
# This file is part of qutebrowser.
|
|
||||||
#
|
|
||||||
# qutebrowser is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# qutebrowser is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
"""Parser for different configuration formats."""
|
|
@ -1,441 +0,0 @@
|
|||||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
|
||||||
|
|
||||||
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
|
||||||
#
|
|
||||||
# This file is part of qutebrowser.
|
|
||||||
#
|
|
||||||
# qutebrowser is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# qutebrowser is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
"""Parser for the key configuration."""
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import os.path
|
|
||||||
import itertools
|
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, QObject
|
|
||||||
|
|
||||||
from qutebrowser.config import configdata, textwrapper
|
|
||||||
from qutebrowser.commands import cmdutils, cmdexc
|
|
||||||
from qutebrowser.utils import log, utils, qtutils, message, usertypes
|
|
||||||
|
|
||||||
|
|
||||||
class KeyConfigError(Exception):
|
|
||||||
|
|
||||||
"""Raised on errors with the key config.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
lineno: The config line in which the exception occurred.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, msg=None):
|
|
||||||
super().__init__(msg)
|
|
||||||
self.lineno = None
|
|
||||||
|
|
||||||
|
|
||||||
class DuplicateKeychainError(KeyConfigError):
|
|
||||||
|
|
||||||
"""Error raised when there's a duplicate key binding."""
|
|
||||||
|
|
||||||
def __init__(self, keychain):
|
|
||||||
super().__init__("Duplicate key chain {}!".format(keychain))
|
|
||||||
self.keychain = keychain
|
|
||||||
|
|
||||||
|
|
||||||
class KeyConfigParser(QObject):
|
|
||||||
|
|
||||||
"""Parser for the keybind config.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
_configfile: The filename of the config or None.
|
|
||||||
_cur_section: The section currently being processed by _read().
|
|
||||||
_cur_command: The command currently being processed by _read().
|
|
||||||
is_dirty: Whether the config is currently dirty.
|
|
||||||
|
|
||||||
Class attributes:
|
|
||||||
UNBOUND_COMMAND: The special command used for unbound keybindings.
|
|
||||||
|
|
||||||
Signals:
|
|
||||||
changed: Emitted when the internal data has changed.
|
|
||||||
arg: Name of the mode which was changed.
|
|
||||||
config_dirty: Emitted when the config should be re-saved.
|
|
||||||
"""
|
|
||||||
|
|
||||||
changed = pyqtSignal(str)
|
|
||||||
config_dirty = pyqtSignal()
|
|
||||||
UNBOUND_COMMAND = '<unbound>'
|
|
||||||
|
|
||||||
def __init__(self, configdir, fname, relaxed=False, parent=None):
|
|
||||||
"""Constructor.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
configdir: The directory to save the configs in.
|
|
||||||
fname: The filename of the config.
|
|
||||||
relaxed: If given, unknown commands are ignored.
|
|
||||||
"""
|
|
||||||
super().__init__(parent)
|
|
||||||
self.is_dirty = False
|
|
||||||
self._cur_section = None
|
|
||||||
self._cur_command = None
|
|
||||||
# Mapping of section name(s) to key binding -> command dicts.
|
|
||||||
self.keybindings = collections.OrderedDict()
|
|
||||||
self._configfile = os.path.join(configdir, fname)
|
|
||||||
|
|
||||||
if not os.path.exists(self._configfile):
|
|
||||||
self._load_default()
|
|
||||||
else:
|
|
||||||
self._read(relaxed)
|
|
||||||
self._load_default(only_new=True)
|
|
||||||
log.init.debug("Loaded bindings: {}".format(self.keybindings))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
"""Get the config as string."""
|
|
||||||
lines = configdata.KEY_FIRST_COMMENT.strip('\n').splitlines()
|
|
||||||
lines.append('')
|
|
||||||
for sectname, sect in self.keybindings.items():
|
|
||||||
lines.append('[{}]'.format(sectname))
|
|
||||||
lines += self._str_section_desc(sectname)
|
|
||||||
lines.append('')
|
|
||||||
data = collections.OrderedDict()
|
|
||||||
for key, cmd in sect.items():
|
|
||||||
if cmd in data:
|
|
||||||
data[cmd].append(key)
|
|
||||||
else:
|
|
||||||
data[cmd] = [key]
|
|
||||||
for cmd, keys in data.items():
|
|
||||||
lines.append(cmd)
|
|
||||||
for k in keys:
|
|
||||||
lines.append(' ' * 4 + k)
|
|
||||||
lines.append('')
|
|
||||||
return '\n'.join(lines) + '\n'
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return utils.get_repr(self, constructor=True,
|
|
||||||
configfile=self._configfile)
|
|
||||||
|
|
||||||
def _str_section_desc(self, sectname):
|
|
||||||
"""Get the section description string for sectname."""
|
|
||||||
wrapper = textwrapper.TextWrapper()
|
|
||||||
lines = []
|
|
||||||
try:
|
|
||||||
seclines = configdata.KEY_SECTION_DESC[sectname].splitlines()
|
|
||||||
except KeyError:
|
|
||||||
return []
|
|
||||||
else:
|
|
||||||
for secline in seclines:
|
|
||||||
if 'http://' in secline or 'https://' in secline:
|
|
||||||
lines.append('# ' + secline)
|
|
||||||
else:
|
|
||||||
lines += wrapper.wrap(secline)
|
|
||||||
return lines
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
"""Save the key config file."""
|
|
||||||
log.destroy.debug("Saving key config to {}".format(self._configfile))
|
|
||||||
|
|
||||||
try:
|
|
||||||
with qtutils.savefile_open(self._configfile,
|
|
||||||
encoding='utf-8') as f:
|
|
||||||
data = str(self)
|
|
||||||
f.write(data)
|
|
||||||
except OSError as e:
|
|
||||||
message.error("Could not save key config: {}".format(e))
|
|
||||||
|
|
||||||
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True,
|
|
||||||
no_replace_variables=True)
|
|
||||||
@cmdutils.argument('command', completion=usertypes.Completion.bind)
|
|
||||||
def bind(self, key, command=None, *, mode='normal', force=False):
|
|
||||||
"""Bind a key to a command.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: The keychain or special key (inside `<...>`) to bind.
|
|
||||||
command: The command to execute, with optional args, or None to
|
|
||||||
print the current binding.
|
|
||||||
mode: A comma-separated list of modes to bind the key in
|
|
||||||
(default: `normal`).
|
|
||||||
force: Rebind the key if it is already bound.
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
if cmd is None:
|
|
||||||
message.info("{} is unbound in {} mode".format(key, mode))
|
|
||||||
else:
|
|
||||||
message.info("{} is bound to '{}' in {} mode".format(key, cmd,
|
|
||||||
mode))
|
|
||||||
return
|
|
||||||
|
|
||||||
modenames = self._normalize_sectname(mode).split(',')
|
|
||||||
for m in modenames:
|
|
||||||
if m not in configdata.KEY_DATA:
|
|
||||||
raise cmdexc.CommandError("Invalid mode {}!".format(m))
|
|
||||||
try:
|
|
||||||
modes = [usertypes.KeyMode[m] for m in modenames]
|
|
||||||
self._validate_command(command, modes)
|
|
||||||
except KeyConfigError as e:
|
|
||||||
raise cmdexc.CommandError(str(e))
|
|
||||||
try:
|
|
||||||
self._add_binding(mode, key, command, force=force)
|
|
||||||
except DuplicateKeychainError as e:
|
|
||||||
raise cmdexc.CommandError("Duplicate keychain {} - use --force to "
|
|
||||||
"override!".format(str(e.keychain)))
|
|
||||||
except KeyConfigError as e:
|
|
||||||
raise cmdexc.CommandError(e)
|
|
||||||
for m in modenames:
|
|
||||||
self.changed.emit(m)
|
|
||||||
self._mark_config_dirty()
|
|
||||||
|
|
||||||
@cmdutils.register(instance='key-config')
|
|
||||||
def unbind(self, key, mode='normal'):
|
|
||||||
"""Unbind a keychain.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: The keychain or special key (inside <...>) to unbind.
|
|
||||||
mode: A comma-separated list of modes to unbind the key in
|
|
||||||
(default: `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:
|
|
||||||
raise cmdexc.CommandError("Invalid mode {}!".format(m))
|
|
||||||
try:
|
|
||||||
sect = self.keybindings[mode]
|
|
||||||
except KeyError:
|
|
||||||
raise cmdexc.CommandError("Can't find mode section '{}'!".format(
|
|
||||||
mode))
|
|
||||||
try:
|
|
||||||
del sect[key]
|
|
||||||
except KeyError:
|
|
||||||
raise cmdexc.CommandError("Can't find binding '{}' in section "
|
|
||||||
"'{}'!".format(key, mode))
|
|
||||||
else:
|
|
||||||
if key in itertools.chain.from_iterable(
|
|
||||||
configdata.KEY_DATA[mode].values()):
|
|
||||||
try:
|
|
||||||
self._add_binding(mode, key, self.UNBOUND_COMMAND)
|
|
||||||
except DuplicateKeychainError:
|
|
||||||
pass
|
|
||||||
for m in mode.split(','):
|
|
||||||
self.changed.emit(m)
|
|
||||||
self._mark_config_dirty()
|
|
||||||
|
|
||||||
def _normalize_sectname(self, s):
|
|
||||||
"""Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'."""
|
|
||||||
if s.startswith('!'):
|
|
||||||
inverted = True
|
|
||||||
s = s[1:]
|
|
||||||
else:
|
|
||||||
inverted = False
|
|
||||||
sections = ','.join(sorted(s.split(',')))
|
|
||||||
if inverted:
|
|
||||||
sections = '!' + sections
|
|
||||||
return sections
|
|
||||||
|
|
||||||
def _load_default(self, *, only_new=False):
|
|
||||||
"""Load the built-in default key bindings.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
only_new: If set, only keybindings which are completely unused
|
|
||||||
(same command/key not bound) are added.
|
|
||||||
"""
|
|
||||||
# {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...}
|
|
||||||
bindings_to_add = collections.OrderedDict()
|
|
||||||
mark_dirty = False
|
|
||||||
|
|
||||||
for sectname, sect in configdata.KEY_DATA.items():
|
|
||||||
sectname = self._normalize_sectname(sectname)
|
|
||||||
bindings_to_add[sectname] = collections.OrderedDict()
|
|
||||||
for command, keychains in sect.items():
|
|
||||||
for e in keychains:
|
|
||||||
if not only_new or self._is_new(sectname, command, e):
|
|
||||||
assert e not in bindings_to_add[sectname]
|
|
||||||
bindings_to_add[sectname][e] = command
|
|
||||||
mark_dirty = True
|
|
||||||
|
|
||||||
for sectname, sect in bindings_to_add.items():
|
|
||||||
if not sect:
|
|
||||||
if not only_new:
|
|
||||||
self.keybindings[sectname] = collections.OrderedDict()
|
|
||||||
else:
|
|
||||||
for keychain, command in sect.items():
|
|
||||||
self._add_binding(sectname, keychain, command)
|
|
||||||
self.changed.emit(sectname)
|
|
||||||
|
|
||||||
if mark_dirty:
|
|
||||||
self._mark_config_dirty()
|
|
||||||
|
|
||||||
def _is_new(self, sectname, command, keychain):
|
|
||||||
"""Check if a given binding is new.
|
|
||||||
|
|
||||||
A binding is considered new if both the command is not bound to any key
|
|
||||||
yet, and the key isn't used anywhere else in the same section.
|
|
||||||
"""
|
|
||||||
if utils.is_special_key(keychain):
|
|
||||||
keychain = keychain.lower()
|
|
||||||
|
|
||||||
try:
|
|
||||||
bindings = self.keybindings[sectname]
|
|
||||||
except KeyError:
|
|
||||||
return True
|
|
||||||
if keychain in bindings:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return command not in bindings.values()
|
|
||||||
|
|
||||||
def _read(self, relaxed=False):
|
|
||||||
"""Read the config file from disk and parse it.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
relaxed: Ignore unknown commands.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
with open(self._configfile, 'r', encoding='utf-8') as f:
|
|
||||||
for i, line in enumerate(f):
|
|
||||||
line = line.rstrip()
|
|
||||||
try:
|
|
||||||
if not line.strip() or line.startswith('#'):
|
|
||||||
continue
|
|
||||||
elif line.startswith('[') and line.endswith(']'):
|
|
||||||
sectname = line[1:-1]
|
|
||||||
self._cur_section = self._normalize_sectname(
|
|
||||||
sectname)
|
|
||||||
elif line.startswith((' ', '\t')):
|
|
||||||
line = line.strip()
|
|
||||||
self._read_keybinding(line)
|
|
||||||
else:
|
|
||||||
line = line.strip()
|
|
||||||
self._read_command(line)
|
|
||||||
except (KeyConfigError, cmdexc.CommandError) as e:
|
|
||||||
if relaxed:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
e.lineno = i
|
|
||||||
raise
|
|
||||||
except OSError:
|
|
||||||
log.keyboard.exception("Failed to read key bindings!")
|
|
||||||
for sectname in self.keybindings:
|
|
||||||
self.changed.emit(sectname)
|
|
||||||
|
|
||||||
def _mark_config_dirty(self):
|
|
||||||
"""Mark the config as dirty."""
|
|
||||||
self.is_dirty = True
|
|
||||||
self.config_dirty.emit()
|
|
||||||
|
|
||||||
def _validate_command(self, line, modes=None):
|
|
||||||
"""Check if a given command is valid.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
line: The commandline to validate.
|
|
||||||
modes: A list of modes to validate the commands for, or None.
|
|
||||||
"""
|
|
||||||
from qutebrowser.config import config
|
|
||||||
if line == self.UNBOUND_COMMAND:
|
|
||||||
return
|
|
||||||
commands = line.split(';;')
|
|
||||||
try:
|
|
||||||
first_cmd = commands[0].split(maxsplit=1)[0].strip()
|
|
||||||
cmd = cmdutils.cmd_dict[first_cmd]
|
|
||||||
if cmd.no_cmd_split:
|
|
||||||
commands = [line]
|
|
||||||
except (KeyError, IndexError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
for cmd in commands:
|
|
||||||
if not cmd.strip():
|
|
||||||
raise KeyConfigError("Got empty command (line: {!r})!".format(
|
|
||||||
line))
|
|
||||||
commands = [c.split(maxsplit=1)[0].strip() for c in commands]
|
|
||||||
for cmd in commands:
|
|
||||||
# FIXME:conf
|
|
||||||
# aliases = config.section('aliases')
|
|
||||||
if cmd in cmdutils.cmd_dict:
|
|
||||||
cmdname = cmd
|
|
||||||
# elif cmd in aliases:
|
|
||||||
# cmdname = aliases[cmd].split(maxsplit=1)[0].strip()
|
|
||||||
else:
|
|
||||||
raise KeyConfigError("Invalid command '{}'!".format(cmd))
|
|
||||||
cmd_obj = cmdutils.cmd_dict[cmdname]
|
|
||||||
for m in modes or []:
|
|
||||||
cmd_obj.validate_mode(m)
|
|
||||||
|
|
||||||
def _read_command(self, line):
|
|
||||||
"""Read a command from a line."""
|
|
||||||
if self._cur_section is None:
|
|
||||||
raise KeyConfigError("Got command '{}' without getting a "
|
|
||||||
"section!".format(line))
|
|
||||||
else:
|
|
||||||
for rgx, repl in configdata.CHANGED_KEY_COMMANDS:
|
|
||||||
if rgx.match(line):
|
|
||||||
line = rgx.sub(repl, line)
|
|
||||||
self._mark_config_dirty()
|
|
||||||
break
|
|
||||||
self._validate_command(line)
|
|
||||||
self._cur_command = line
|
|
||||||
|
|
||||||
def _read_keybinding(self, line):
|
|
||||||
"""Read a key binding from a line."""
|
|
||||||
if self._cur_command is None:
|
|
||||||
raise KeyConfigError("Got key binding '{}' without getting a "
|
|
||||||
"command!".format(line))
|
|
||||||
else:
|
|
||||||
assert self._cur_section is not None
|
|
||||||
self._add_binding(self._cur_section, line, self._cur_command)
|
|
||||||
|
|
||||||
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:
|
|
||||||
self.keybindings[sectname] = collections.OrderedDict()
|
|
||||||
if keychain in self.get_bindings_for(sectname):
|
|
||||||
if force or command == self.UNBOUND_COMMAND:
|
|
||||||
self.unbind(keychain, mode=sectname)
|
|
||||||
else:
|
|
||||||
raise DuplicateKeychainError(keychain)
|
|
||||||
section = self.keybindings[sectname]
|
|
||||||
if (command != self.UNBOUND_COMMAND and
|
|
||||||
section.get(keychain, None) == self.UNBOUND_COMMAND):
|
|
||||||
# re-binding an unbound keybinding
|
|
||||||
del section[keychain]
|
|
||||||
self.keybindings[sectname][keychain] = command
|
|
||||||
|
|
||||||
def get_bindings_for(self, section):
|
|
||||||
"""Get a dict with all merged key bindings for a section."""
|
|
||||||
bindings = {}
|
|
||||||
for sectstring, d in self.keybindings.items():
|
|
||||||
if sectstring.startswith('!'):
|
|
||||||
inverted = True
|
|
||||||
sectstring = sectstring[1:]
|
|
||||||
else:
|
|
||||||
inverted = False
|
|
||||||
sects = [s.strip() for s in sectstring.split(',')]
|
|
||||||
matches = any(s == section for s in sects)
|
|
||||||
if (not inverted and matches) or (inverted and not matches):
|
|
||||||
bindings.update(d)
|
|
||||||
try:
|
|
||||||
bindings.update(self.keybindings['all'])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
bindings = {k: v for k, v in bindings.items()
|
|
||||||
if v != self.UNBOUND_COMMAND}
|
|
||||||
return bindings
|
|
@ -46,11 +46,9 @@ Feature: Using completion
|
|||||||
When I run :set-cmd-text -s :bookmark-load
|
When I run :set-cmd-text -s :bookmark-load
|
||||||
Then the completion model should be BookmarkCompletionModel
|
Then the completion model should be BookmarkCompletionModel
|
||||||
|
|
||||||
# FIXME:conf
|
Scenario: Using bind completion
|
||||||
|
When I run :set-cmd-text -s :bind X
|
||||||
# Scenario: Using bind completion
|
Then the completion model should be BindCompletionModel
|
||||||
# When I run :set-cmd-text -s :bind X
|
|
||||||
# Then the completion model should be BindCompletionModel
|
|
||||||
|
|
||||||
Scenario: Using session completion
|
Scenario: Using session completion
|
||||||
Given I open data/hello.txt
|
Given I open data/hello.txt
|
||||||
|
@ -243,32 +243,30 @@ Feature: Using hints
|
|||||||
|
|
||||||
### hints.auto_follow-timeout
|
### hints.auto_follow-timeout
|
||||||
|
|
||||||
## FIXME:conf
|
@not_osx
|
||||||
|
Scenario: Ignoring key presses after auto-following hints
|
||||||
|
When I set hints.auto_follow_timeout to 1000
|
||||||
|
And I set hints.mode to number
|
||||||
|
And I run :bind --force , message-error "This error message was triggered via a keybinding which should have been inhibited"
|
||||||
|
And I open data/hints/html/simple.html
|
||||||
|
And I hint with args "all"
|
||||||
|
And I press the key "f"
|
||||||
|
And I wait until data/hello.txt is loaded
|
||||||
|
And I press the key ","
|
||||||
|
# Waiting here so we don't affect the next test
|
||||||
|
And I wait for "Releasing inhibition state of normal mode." in the log
|
||||||
|
Then "Ignoring key ',', because the normal mode is currently inhibited." should be logged
|
||||||
|
|
||||||
# @not_osx
|
Scenario: Turning off auto-follow-timeout
|
||||||
# Scenario: Ignoring key presses after auto-following hints
|
When I set hints.auto_follow_timeout to 0
|
||||||
# When I set hints.auto_follow_timeout to 1000
|
And I set hints.mode to number
|
||||||
# And I set hints.mode to number
|
And I run :bind --force , message-info "Keypress worked!"
|
||||||
# And I run :bind --force , message-error "This error message was triggered via a keybinding which should have been inhibited"
|
And I open data/hints/html/simple.html
|
||||||
# And I open data/hints/html/simple.html
|
And I hint with args "all"
|
||||||
# And I hint with args "all"
|
And I press the key "f"
|
||||||
# And I press the key "f"
|
And I wait until data/hello.txt is loaded
|
||||||
# And I wait until data/hello.txt is loaded
|
And I press the key ","
|
||||||
# And I press the key ","
|
Then the message "Keypress worked!" should be shown
|
||||||
# # Waiting here so we don't affect the next test
|
|
||||||
# And I wait for "Releasing inhibition state of normal mode." in the log
|
|
||||||
# Then "Ignoring key ',', because the normal mode is currently inhibited." should be logged
|
|
||||||
|
|
||||||
# Scenario: Turning off auto-follow-timeout
|
|
||||||
# When I set hints.auto_follow_timeout to 0
|
|
||||||
# And I set hints.mode to number
|
|
||||||
# And I run :bind --force , message-info "Keypress worked!"
|
|
||||||
# And I open data/hints/html/simple.html
|
|
||||||
# And I hint with args "all"
|
|
||||||
# And I press the key "f"
|
|
||||||
# And I wait until data/hello.txt is loaded
|
|
||||||
# And I press the key ","
|
|
||||||
# Then the message "Keypress worked!" should be shown
|
|
||||||
|
|
||||||
### Word hints
|
### Word hints
|
||||||
|
|
||||||
|
@ -21,7 +21,3 @@ import pytest
|
|||||||
|
|
||||||
import pytest_bdd as bdd
|
import pytest_bdd as bdd
|
||||||
bdd.scenarios('keyinput.feature')
|
bdd.scenarios('keyinput.feature')
|
||||||
|
|
||||||
## FIXME:conf
|
|
||||||
pytestmark = pytest.mark.skipif(True, reason="FIXME:conf")
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user