Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(binding): fix comma-space separated keymaps #5700

Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed

- Fixed markup escaping edge cases https://github.com/Textualize/textual/pull/5697
- Fixed keymap bindings with comma-space separated keys https://github.com/Textualize/textual/issues/5694
- Fixed keymap bindings with symbol keys https://github.com/Textualize/textual/issues/5704

### Changed

Expand Down
27 changes: 17 additions & 10 deletions src/textual/binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ class InvalidBinding(Exception):
"""Binding key is in an invalid format."""


def _key_string_to_keys(key_string: str) -> list[str]:
"""Convert a key binding string into a list of key values."""
keys: list[str] = []
for key in key_string.split(","):
key = key.strip()
if not key:
raise InvalidBinding(f"Can not bind empty string in {key_string!r}")
if len(key) == 1:
key = _character_to_key(key)

keys.append(key)

return keys


@dataclass(frozen=True)
class Binding:
"""The configuration of a key binding."""
Expand Down Expand Up @@ -132,15 +147,7 @@ def make_bindings(cls, bindings: Iterable[BindingType]) -> Iterable[Binding]:
# At this point we have a Binding instance, but the key may
# be a list of keys, so now we unroll that single Binding
# into a (potential) collection of Binding instances.
for key in binding.key.split(","):
key = key.strip()
if not key:
raise InvalidBinding(
f"Can not bind empty string in {binding.key!r}"
)
if len(key) == 1:
key = _character_to_key(key)

for key in _key_string_to_keys(binding.key):
yield Binding(
key=key,
action=binding.action,
Expand Down Expand Up @@ -277,7 +284,7 @@ def apply_keymap(self, keymap: Keymap) -> KeymapApplyResult:

# If the keymap has an override for this binding ID
if keymap_key_string := keymap.get(binding_id):
keymap_keys = keymap_key_string.split(",")
keymap_keys = _key_string_to_keys(keymap_key_string)

# Remove the old binding
for key, key_bindings in key_to_bindings:
Expand Down
54 changes: 54 additions & 0 deletions tests/test_keymap.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,57 @@ def on_mount(self) -> None:
await pilot.press("i")
assert parent_counter == 1
assert child_counter == 1


async def test_keymap_with_comma_space_separated_keys():
"""Regression test for https://github.com/Textualize/textual/issues/5694"""

class KeymapApp(App):
BINDINGS = [
Binding(key="i, up", action="increment", id="increment"),
# Note the existing binding is comma-space separated
]

counter = 0

def action_increment(self) -> None:
self.counter += 1

app = KeymapApp()
async with app.run_test() as pilot:
# Sanity check
await pilot.press("i")
await pilot.press("up")
assert app.counter == 2

# Update the keymap with the same comma-separated binding as before
app.update_keymap({"increment": "i, up"})
await pilot.press("i")
await pilot.press("up")
assert app.counter == 4


async def test_keymap_with_symbol_keys():
"""Regression test for https://github.com/Textualize/textual/issues/5704"""

class KeymapApp(App):
BINDINGS = [
Binding(key="+", action="increment", id="increment"),
# Note the existing binding is the symbol key "+" rather than "plus"
]

counter = 0

def action_increment(self) -> None:
self.counter += 1

app = KeymapApp()
async with app.run_test() as pilot:
# Sanity check
await pilot.press("+")
assert app.counter == 1

# Update the keymap with the same symbol key as before
app.update_keymap({"increment": "+"})
await pilot.press("+")
assert app.counter == 2
Loading