diff --git a/src/app_model/types/__init__.py b/src/app_model/types/__init__.py index 52e6609f..49da2589 100644 --- a/src/app_model/types/__init__.py +++ b/src/app_model/types/__init__.py @@ -4,6 +4,7 @@ from ._action import Action from ._command_rule import CommandRule, ToggleRule +from ._constants import OperatingSystem from ._icon import Icon from ._keybinding_rule import KeyBindingRule from ._keys import ( @@ -41,6 +42,7 @@ "KeyCode", "KeyCombo", "KeyMod", + "OperatingSystem", "MenuItem", "MenuItemBase", "MenuRule", diff --git a/src/app_model/types/_keys/_keybindings.py b/src/app_model/types/_keys/_keybindings.py index ae63843b..215bebe3 100644 --- a/src/app_model/types/_keys/_keybindings.py +++ b/src/app_model/types/_keys/_keybindings.py @@ -11,13 +11,6 @@ from pydantic.annotated_handlers import GetCoreSchemaHandler from pydantic_core import core_schema -_re_ctrl = re.compile(r"ctrl[\+|\-]") -_re_shift = re.compile(r"shift[\+|\-]") -_re_alt = re.compile(r"alt[\+|\-]") -_re_meta = re.compile(r"meta[\+|\-]") -_re_win = re.compile(r"win[\+|\-]") -_re_cmd = re.compile(r"cmd[\+|\-]") - class SimpleKeyBinding(BaseModel): """Represent a simple combination modifier(s) and a key, e.g. Ctrl+A.""" @@ -251,6 +244,12 @@ def validate(cls, v: Any) -> "KeyBinding": raise TypeError("invalid keybinding") # pragma: no cover +_re_ctrl = re.compile(r"(ctrl|control|ctl|⌃|\^)[\+|\-]") +_re_shift = re.compile(r"(shift|⇧)[\+|\-]") +_re_alt = re.compile(r"(alt|opt|option|⌥)[\+|\-]") +_re_meta = re.compile(r"(meta|super|win|windows|⊞|cmd|command|⌘)[\+|\-]") + + def _parse_modifiers(input: str) -> Tuple[Dict[str, bool], str]: """Parse modifiers from a string (case insensitive). @@ -259,38 +258,17 @@ def _parse_modifiers(input: str) -> Tuple[Dict[str, bool], str]: """ remainder = input.lower() - ctrl = False - shift = False - alt = False - meta = False - + patterns = {"ctrl": _re_ctrl, "shift": _re_shift, "alt": _re_alt, "meta": _re_meta} + mods = dict.fromkeys(patterns, False) while True: saw_modifier = False - if _re_ctrl.match(remainder): - remainder = remainder[5:] - ctrl = True - saw_modifier = True - if _re_shift.match(remainder): - remainder = remainder[6:] - shift = True - saw_modifier = True - if _re_alt.match(remainder): - remainder = remainder[4:] - alt = True - saw_modifier = True - if _re_meta.match(remainder): - remainder = remainder[5:] - meta = True - saw_modifier = True - if _re_win.match(remainder): - remainder = remainder[4:] - meta = True - saw_modifier = True - if _re_cmd.match(remainder): - remainder = remainder[4:] - meta = True - saw_modifier = True + for key, ptrn in patterns.items(): + if m := ptrn.match(remainder): + remainder = remainder[m.span()[1] :] + mods[key] = True + saw_modifier = True + break if not saw_modifier: break - return {"ctrl": ctrl, "shift": shift, "alt": alt, "meta": meta}, remainder + return mods, remainder diff --git a/tests/test_keybindings.py b/tests/test_keybindings.py index b1f40934..b714d793 100644 --- a/tests/test_keybindings.py +++ b/tests/test_keybindings.py @@ -1,3 +1,4 @@ +import itertools import sys from typing import ClassVar @@ -45,7 +46,7 @@ def test_simple_keybinding_single_mod(mod: str, key: str) -> None: assert int(as_full_kb) == int(kb) -def test_simple_keybinding_multi_mod(): +def test_simple_keybinding_multi_mod() -> None: # here we're also testing that cmd and win get cast to 'KeyMod.CtrlCmd' kb = SimpleKeyBinding.from_str("cmd+shift+A") @@ -60,6 +61,24 @@ def test_simple_keybinding_multi_mod(): assert kb.is_modifier_key() +controls = ["ctrl", "control", "ctl", "⌃", "^"] +shifts = ["shift", "⇧"] +alts = ["alt", "opt", "option", "⌥"] +metas = ["meta", "super", "cmd", "command", "⌘", "win", "windows", "⊞"] +delimiters = ["+", "-"] +key = ["A"] +combos = [ + delim.join(x) + for delim, *x in itertools.product(delimiters, controls, shifts, alts, metas, key) +] + + +@pytest.mark.parametrize("key", combos) +def test_keybinding_parser(key: str) -> None: + # Test all the different ways to write the modifiers + assert str(KeyBinding.from_str(key)) == "Ctrl+Shift+Alt+Meta+A" + + def test_chord_keybinding() -> None: kb = KeyBinding.from_str("Shift+A Cmd+9") assert len(kb) == 2 @@ -75,7 +94,7 @@ def test_chord_keybinding() -> None: assert KeyBinding.validate(kb) == kb -def test_in_dict(): +def test_in_dict() -> None: a = SimpleKeyBinding.from_str("Shift+A") b = KeyBinding.from_str("Shift+B") @@ -99,7 +118,7 @@ def test_in_dict(): kbs[new_a] -def test_in_model(): +def test_in_model() -> None: class M(BaseModel): key: KeyBinding @@ -113,7 +132,7 @@ class Config: assert m.model_dump_json().replace('": "', '":"') == '{"key":"Shift+A B"}' -def test_standard_keybindings(): +def test_standard_keybindings() -> None: class M(BaseModel): key: KeyBindingRule