Initial :bind/:unbind implementation

This commit is contained in:
Florian Bruhin 2017-06-19 16:41:17 +02:00
parent 290d27a064
commit ba1bc29a97
10 changed files with 174 additions and 511 deletions

View File

@ -184,7 +184,7 @@ def _process_args(args):
config_obj = objreg.get('config')
for opt, val in args.temp_settings:
try:
config_obj.set(opt, val)
config_obj.set_str(opt, val)
except configexc.Error as e:
message.error("set: {} - {}".format(e.__class__.__name__, e))

View File

@ -457,7 +457,7 @@ def _qute_settings_set(url):
return 'text/html', b'error: ' + msg.encode('utf-8')
try:
config.instance.set(option, value)
config.instance.set_str(option, value)
return 'text/html', b'ok'
except configexc.Error as e:
message.error(str(e))

View File

@ -27,8 +27,9 @@ import configparser
from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings
from qutebrowser.config import configdata, configexc, configtypes
from qutebrowser.utils import utils, objreg, message, standarddir, log
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import (utils, objreg, message, standarddir, log,
usertypes)
from qutebrowser.commands import cmdexc, cmdutils, runners
# 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:
def get_reverse_bindings_for(self, section):
"""Get a dict of commands to a list of bindings for the section."""
def __init__(self, manager):
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 = {}
bindings = val.bindings.commands[section]
bindings = val.bindings.commands[mode]
if bindings is None:
return cmd_to_keys
for key, full_cmd in bindings.items():
@ -136,6 +140,57 @@ class NewKeyConfig:
cmd_to_keys[cmd].insert(0, key)
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:
@ -200,7 +255,7 @@ class ConfigCommands:
if len(values) == 1:
# If we have only one value, just set it directly (avoid
# breaking stuff like aliases or other pseudo-settings)
self._config.set(option, values[0])
self._config.set_str(option, values[0])
return
# Use the next valid value from values, or the first if the current
@ -212,7 +267,7 @@ class ConfigCommands:
value = values[idx]
except ValueError:
value = values[0]
self._config.set(option, value)
self._config.set_str(option, value)
@contextlib.contextmanager
def _handle_config_error(self):
@ -225,6 +280,56 @@ class ConfigCommands:
raise cmdexc.CommandError("set: {} - {}".format(
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):
@ -235,6 +340,10 @@ class NewConfigManager(QObject):
self.options = {}
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):
for name, option in configdata.DATA.items():
self.options[name] = option
@ -256,11 +365,17 @@ class NewConfigManager(QObject):
return opt.typ.to_str(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
opt = self.get_opt(name)
self._values[name] = opt.typ.from_str(value)
self.changed.emit(name)
log.config.debug("Config option changed: {} = {}".format(name, value))
self._changed(name, value)
def dump_userconfig(self):
"""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
options.
"""
if attr.startswith('_'):
return self.__getattribute__(attr)
name = self._join(attr)
if configdata.is_valid_prefix(name):
return ConfigContainer(manager=self._manager, prefix=name)
try:
return self._manager.get(name)
except configexc.NoOptionError as e:
@ -313,7 +432,7 @@ class ConfigContainer:
def __setattr__(self, attr, value):
if attr.startswith('_'):
return super().__setattr__(attr, value)
self._handler(self._join(attr), value)
self._manager.set(self._join(attr), value)
def _join(self, attr):
if self._prefix:
@ -364,7 +483,7 @@ def init(parent=None):
global val, instance, key_instance
val = ConfigContainer(new_config)
instance = new_config
key_instance = NewKeyConfig()
key_instance = NewKeyConfig(new_config)
for cf in _change_filters:
cf.validate()

View File

@ -52,6 +52,13 @@ class ValidationError(Error):
self.option = None
class DuplicateKeyError(ValidationError):
"""Raised when there was a duplicate key."""
pass
class NoOptionError(Error):
"""Raised when an option was not found."""

View File

@ -729,9 +729,15 @@ class Command(BaseType):
self._basic_py_validation(value, str)
if not value:
return
split = value.split()
if not split or split[0] not in cmdutils.cmd_dict:
raise configexc.ValidationError(value, "must be a valid command!")
# FIXME:conf is it okay to import runners.py here?
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
def complete(self):

View File

@ -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."""

View File

@ -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

View File

@ -46,11 +46,9 @@ Feature: Using completion
When I run :set-cmd-text -s :bookmark-load
Then the completion model should be BookmarkCompletionModel
# FIXME:conf
# Scenario: Using bind completion
# When I run :set-cmd-text -s :bind X
# Then the completion model should be BindCompletionModel
Scenario: Using bind completion
When I run :set-cmd-text -s :bind X
Then the completion model should be BindCompletionModel
Scenario: Using session completion
Given I open data/hello.txt

View File

@ -243,32 +243,30 @@ Feature: Using hints
### 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: 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
# 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
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

View File

@ -21,7 +21,3 @@ import pytest
import pytest_bdd as bdd
bdd.scenarios('keyinput.feature')
## FIXME:conf
pytestmark = pytest.mark.skipif(True, reason="FIXME:conf")