Files
kitty-mirror/kitty/key_encoding.py
2025-02-03 10:56:50 +05:30

429 lines
12 KiB
Python
Generated

#!/usr/bin/env python
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
from enum import IntEnum
from functools import lru_cache
from typing import NamedTuple
from . import fast_data_types as defines
from .fast_data_types import KeyEvent as WindowSystemKeyEvent
from .key_names import character_key_name_aliases, functional_key_name_aliases
from .types import ParsedShortcut
# number name mappings {{{
# start csi mapping (auto generated by gen-key-constants.py do not edit)
functional_key_number_to_name_map = {
57344: 'ESCAPE',
57345: 'ENTER',
57346: 'TAB',
57347: 'BACKSPACE',
57348: 'INSERT',
57349: 'DELETE',
57350: 'LEFT',
57351: 'RIGHT',
57352: 'UP',
57353: 'DOWN',
57354: 'PAGE_UP',
57355: 'PAGE_DOWN',
57356: 'HOME',
57357: 'END',
57358: 'CAPS_LOCK',
57359: 'SCROLL_LOCK',
57360: 'NUM_LOCK',
57361: 'PRINT_SCREEN',
57362: 'PAUSE',
57363: 'MENU',
57364: 'F1',
57365: 'F2',
57366: 'F3',
57367: 'F4',
57368: 'F5',
57369: 'F6',
57370: 'F7',
57371: 'F8',
57372: 'F9',
57373: 'F10',
57374: 'F11',
57375: 'F12',
57376: 'F13',
57377: 'F14',
57378: 'F15',
57379: 'F16',
57380: 'F17',
57381: 'F18',
57382: 'F19',
57383: 'F20',
57384: 'F21',
57385: 'F22',
57386: 'F23',
57387: 'F24',
57388: 'F25',
57389: 'F26',
57390: 'F27',
57391: 'F28',
57392: 'F29',
57393: 'F30',
57394: 'F31',
57395: 'F32',
57396: 'F33',
57397: 'F34',
57398: 'F35',
57399: 'KP_0',
57400: 'KP_1',
57401: 'KP_2',
57402: 'KP_3',
57403: 'KP_4',
57404: 'KP_5',
57405: 'KP_6',
57406: 'KP_7',
57407: 'KP_8',
57408: 'KP_9',
57409: 'KP_DECIMAL',
57410: 'KP_DIVIDE',
57411: 'KP_MULTIPLY',
57412: 'KP_SUBTRACT',
57413: 'KP_ADD',
57414: 'KP_ENTER',
57415: 'KP_EQUAL',
57416: 'KP_SEPARATOR',
57417: 'KP_LEFT',
57418: 'KP_RIGHT',
57419: 'KP_UP',
57420: 'KP_DOWN',
57421: 'KP_PAGE_UP',
57422: 'KP_PAGE_DOWN',
57423: 'KP_HOME',
57424: 'KP_END',
57425: 'KP_INSERT',
57426: 'KP_DELETE',
57427: 'KP_BEGIN',
57428: 'MEDIA_PLAY',
57429: 'MEDIA_PAUSE',
57430: 'MEDIA_PLAY_PAUSE',
57431: 'MEDIA_REVERSE',
57432: 'MEDIA_STOP',
57433: 'MEDIA_FAST_FORWARD',
57434: 'MEDIA_REWIND',
57435: 'MEDIA_TRACK_NEXT',
57436: 'MEDIA_TRACK_PREVIOUS',
57437: 'MEDIA_RECORD',
57438: 'LOWER_VOLUME',
57439: 'RAISE_VOLUME',
57440: 'MUTE_VOLUME',
57441: 'LEFT_SHIFT',
57442: 'LEFT_CONTROL',
57443: 'LEFT_ALT',
57444: 'LEFT_SUPER',
57445: 'LEFT_HYPER',
57446: 'LEFT_META',
57447: 'RIGHT_SHIFT',
57448: 'RIGHT_CONTROL',
57449: 'RIGHT_ALT',
57450: 'RIGHT_SUPER',
57451: 'RIGHT_HYPER',
57452: 'RIGHT_META',
57453: 'ISO_LEVEL3_SHIFT',
57454: 'ISO_LEVEL5_SHIFT'}
csi_number_to_functional_number_map = {
2: 57348,
3: 57349,
5: 57354,
6: 57355,
7: 57356,
8: 57357,
9: 57346,
11: 57364,
12: 57365,
13: 57345,
14: 57367,
15: 57368,
17: 57369,
18: 57370,
19: 57371,
20: 57372,
21: 57373,
23: 57374,
24: 57375,
27: 57344,
127: 57347}
letter_trailer_to_csi_number_map = {'A': 57352, 'B': 57353, 'C': 57351, 'D': 57350, 'E': 57427, 'F': 8, 'H': 7, 'P': 11, 'Q': 12, 'S': 14}
tilde_trailers = {57348, 57349, 57354, 57355, 57366, 57368, 57369, 57370, 57371, 57372, 57373, 57374, 57375}
# end csi mapping
# }}}
@lru_cache(2)
def get_name_to_functional_number_map() -> dict[str, int]:
return {v: k for k, v in functional_key_number_to_name_map.items()}
@lru_cache(2)
def get_functional_to_csi_number_map() -> dict[int, int]:
return {v: k for k, v in csi_number_to_functional_number_map.items()}
@lru_cache(2)
def get_csi_number_to_letter_trailer_map() -> dict[int, str]:
return {v: k for k, v in letter_trailer_to_csi_number_map.items()}
PRESS: int = 1
REPEAT: int = 2
RELEASE: int = 4
class EventType(IntEnum):
PRESS = PRESS
REPEAT = REPEAT
RELEASE = RELEASE
@lru_cache(maxsize=128)
def parse_shortcut(spec: str) -> ParsedShortcut:
if spec.endswith('+'):
spec = f'{spec[:-1]}plus'
parts = spec.split('+')
key_name = parts[-1]
key_name = functional_key_name_aliases.get(key_name.upper(), key_name)
is_functional_key = key_name.upper() in get_name_to_functional_number_map()
if is_functional_key:
key_name = key_name.upper()
else:
key_name = character_key_name_aliases.get(key_name.upper(), key_name)
mod_val = 0
if len(parts) > 1:
mods = tuple(config_mod_map.get(x.upper(), META << 8) for x in parts[:-1])
for x in mods:
mod_val |= x
return ParsedShortcut(mod_val, key_name)
class KeyEvent(NamedTuple):
type: EventType = EventType.PRESS
mods: int = 0
key: str = ''
text: str = ''
shifted_key: str = ''
alternate_key: str = ''
shift: bool = False
alt: bool = False
ctrl: bool = False
super: bool = False
hyper: bool = False
meta: bool = False
caps_lock: bool = False
num_lock: bool = False
def matches(self, spec: str | ParsedShortcut, types: int = EventType.PRESS | EventType.REPEAT) -> bool:
mods = self.mods_without_locks
if not self.type & types:
return False
if isinstance(spec, str):
spec = parse_shortcut(spec)
if (mods, self.key) == spec:
return True
is_shifted = bool(self.shifted_key and self.shift)
if is_shifted and (mods & ~SHIFT, self.shifted_key) == spec:
return True
return False
def matches_without_mods(self, spec: str | ParsedShortcut, types: int = EventType.PRESS | EventType.REPEAT) -> bool:
if not self.type & types:
return False
if isinstance(spec, str):
spec = parse_shortcut(spec)
return self.key == spec[1]
def matches_text(self, text: str, case_sensitive: bool = False) -> bool:
if case_sensitive:
return self.text == text
return self.text.lower() == text.lower()
@property
def is_release(self) -> bool:
return self.type is EventType.RELEASE
@property
def mods_without_locks(self) -> int:
return self.mods & ~(NUM_LOCK | CAPS_LOCK)
@property
def has_mods(self) -> bool:
return bool(self.mods_without_locks)
def as_window_system_event(self) -> WindowSystemKeyEvent:
action = defines.GLFW_PRESS
if self.type is EventType.REPEAT:
action = defines.GLFW_REPEAT
elif self.type is EventType.RELEASE:
action = defines.GLFW_RELEASE
mods = 0
if self.mods:
if self.shift:
mods |= defines.GLFW_MOD_SHIFT
if self.alt:
mods |= defines.GLFW_MOD_ALT
if self.ctrl:
mods |= defines.GLFW_MOD_CONTROL
if self.super:
mods |= defines.GLFW_MOD_SUPER
if self.hyper:
mods |= defines.GLFW_MOD_HYPER
if self.meta:
mods |= defines.GLFW_MOD_META
if self.caps_lock:
mods |= defines.GLFW_MOD_CAPS_LOCK
if self.num_lock:
mods |= defines.GLFW_MOD_NUM_LOCK
fnm = get_name_to_functional_number_map()
def as_num(key: str) -> int:
return (fnm.get(key) or ord(key)) if key else 0
return WindowSystemKeyEvent(
key=as_num(self.key), shifted_key=as_num(self.shifted_key),
alternate_key=as_num(self.alternate_key), mods=mods,
action=action, text=self.text)
SHIFT, ALT, CTRL, SUPER, HYPER, META, CAPS_LOCK, NUM_LOCK = 1, 2, 4, 8, 16, 32, 64, 128
enter_key = KeyEvent(key='ENTER')
backspace_key = KeyEvent(key='BACKSPACE')
config_mod_map = {
'SHIFT': SHIFT,
'': SHIFT,
'ALT': ALT,
'OPTION': ALT,
'OPT': ALT,
'': ALT,
'SUPER': SUPER,
'COMMAND': SUPER,
'CMD': SUPER,
'': SUPER,
'CONTROL': CTRL,
'CTRL': CTRL,
'': CTRL,
'HYPER': HYPER,
'META': META,
'NUM_LOCK': NUM_LOCK,
'CAPS_LOCK': CAPS_LOCK,
}
def decode_key_event(csi: str, csi_type: str) -> KeyEvent:
parts = csi.split(';')
def get_sub_sections(x: str, missing: int = 0) -> tuple[int, ...]:
return tuple(int(y) if y else missing for y in x.split(':'))
first_section = get_sub_sections(parts[0])
second_section = get_sub_sections(parts[1], 1) if len(parts) > 1 else ()
third_section = get_sub_sections(parts[2]) if len(parts) > 2 else ()
mods = (second_section[0] - 1) if second_section else 0
action = second_section[1] if len(second_section) > 1 else 1
keynum = first_section[0]
if csi_type in 'ABCDEHFPQRS':
keynum = letter_trailer_to_csi_number_map[csi_type]
def key_name(num: int) -> str:
if not num:
return ''
if num != 13:
num = csi_number_to_functional_number_map.get(num, num)
ans = functional_key_number_to_name_map.get(num)
else:
ans = 'ENTER' if csi_type == 'u' else 'F3'
if ans is None:
ans = chr(num)
return ans
return KeyEvent(
mods=mods, shift=bool(mods & SHIFT), alt=bool(mods & ALT),
ctrl=bool(mods & CTRL), super=bool(mods & SUPER),
hyper=bool(mods & HYPER), meta=bool(mods & META),
caps_lock=bool(mods & CAPS_LOCK), num_lock=bool(mods & NUM_LOCK),
key=key_name(keynum),
shifted_key=key_name(first_section[1] if len(first_section) > 1 else 0),
alternate_key=key_name(first_section[2] if len(first_section) > 2 else 0),
type={1: EventType.PRESS, 2: EventType.REPEAT, 3: EventType.RELEASE}[action],
text=''.join(map(chr, third_section))
)
def csi_number_for_name(key_name: str) -> int:
if not key_name:
return 0
if key_name in ('F3', 'ENTER'):
return 13
fn = get_name_to_functional_number_map().get(key_name)
if fn is None:
return ord(key_name)
return get_functional_to_csi_number_map().get(fn, fn)
def encode_key_event(key_event: KeyEvent) -> str:
key = csi_number_for_name(key_event.key)
shifted_key = csi_number_for_name(key_event.shifted_key)
alternate_key = csi_number_for_name(key_event.alternate_key)
lt = get_csi_number_to_letter_trailer_map()
if key_event.key == 'ENTER':
trailer = 'u'
else:
trailer = lt.get(key, 'u')
if trailer != 'u':
key = 1
mods = key_event.mods
text = key_event.text
ans = '\033['
if key != 1 or mods or shifted_key or alternate_key or text:
ans += f'{key}'
if shifted_key or alternate_key:
ans += ':' + (f'{shifted_key}' if shifted_key else '')
if alternate_key:
ans += f':{alternate_key}'
action = 1
if key_event.type is EventType.REPEAT:
action = 2
elif key_event.type is EventType.RELEASE:
action = 3
if mods or action > 1 or text:
m = 0
if key_event.shift:
m |= 1
if key_event.alt:
m |= 2
if key_event.ctrl:
m |= 4
if key_event.super:
m |= 8
if key_event.hyper:
m |= 16
if key_event.meta:
m |= 32
if key_event.caps_lock:
m |= 64
if key_event.num_lock:
m |= 128
if action > 1 or m:
ans += f';{m+1}'
if action > 1:
ans += f':{action}'
elif text:
ans += ';'
if text:
ans += ';' + ':'.join(map(str, map(ord, text)))
fn = get_name_to_functional_number_map().get(key_event.key)
if fn is not None and fn in tilde_trailers:
trailer = '~'
return ans + trailer
def decode_key_event_as_window_system_key(text: str) -> WindowSystemKeyEvent | None:
csi, trailer = text[2:-1], text[-1]
try:
k = decode_key_event(csi, trailer)
except Exception:
return None
return k.as_window_system_event()