Coverage for src / lexigram / admin / actions / shortcuts.py: 0%

30 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-11 02:25 +0800

1"""Generic keyboard shortcut manager for action types. 

2 

3Provides a reusable :class:`KeyboardShortcutManager` that works with any 

4action type that has ``keyboard_shortcut`` and ``name`` attributes. 

5""" 

6 

7from __future__ import annotations 

8 

9from typing import Generic, Protocol, TypeVar 

10 

11 

12class _HasShortcut(Protocol): 

13 """Minimal protocol for actions that support keyboard shortcuts.""" 

14 

15 keyboard_shortcut: str | None 

16 name: str 

17 

18 

19ActionT = TypeVar("ActionT", bound=_HasShortcut) 

20 

21 

22class KeyboardShortcutManager(Generic[ActionT]): 

23 """Manages keyboard shortcuts for any action type. 

24 

25 Handles shortcut registration, lookup, and formatting. Sub-managers 

26 extend this class to add type-specific execution logic (e.g., passing 

27 a ``record_id`` for row actions). 

28 """ 

29 

30 def __init__(self) -> None: 

31 """Initialize the shortcut manager.""" 

32 self._shortcuts: dict[str, ActionT] = {} 

33 

34 def register_action(self, action: ActionT) -> None: 

35 """Register an action under its keyboard shortcut.""" 

36 if action.keyboard_shortcut: 

37 self._shortcuts[action.keyboard_shortcut] = action 

38 

39 def unregister_action(self, shortcut: str) -> None: 

40 """Remove a shortcut registration.""" 

41 self._shortcuts.pop(shortcut, None) 

42 

43 def get_action_for_shortcut(self, shortcut: str) -> ActionT | None: 

44 """Return the action bound to *shortcut*, or ``None``.""" 

45 return self._shortcuts.get(shortcut) 

46 

47 def get_registered_shortcuts(self) -> dict[str, str]: 

48 """Return a mapping of shortcut → action name.""" 

49 return {sc: action.name for sc, action in self._shortcuts.items()} 

50 

51 def is_shortcut_registered(self, shortcut: str) -> bool: 

52 """Return ``True`` if *shortcut* is currently registered.""" 

53 return shortcut in self._shortcuts 

54 

55 def clear_all_shortcuts(self) -> None: 

56 """Remove all registered shortcuts.""" 

57 self._shortcuts.clear() 

58 

59 @staticmethod 

60 def normalize_shortcut(shortcut: str) -> str: 

61 """Normalise a shortcut string to lowercase with no spaces. 

62 

63 Examples: 

64 ``'Ctrl+R'`` → ``'ctrl+r'`` 

65 """ 

66 return shortcut.lower().replace(" ", "") 

67 

68 @staticmethod 

69 def format_shortcut(shortcut: str) -> str: 

70 """Format a normalised shortcut for display. 

71 

72 Examples: 

73 ``'ctrl+r'`` → ``'Ctrl+R'`` 

74 ``'delete'`` → ``'Del'`` 

75 """ 

76 _SPECIAL = { 

77 "ctrl": "Ctrl", 

78 "cmd": "Cmd", 

79 "alt": "Alt", 

80 "shift": "Shift", 

81 "delete": "Del", 

82 } 

83 parts = shortcut.split("+") 

84 return "+".join(_SPECIAL.get(p, p.upper()) for p in parts)