diff --git a/docs/keyboard-protocol.rst b/docs/keyboard-protocol.rst index 73c87accf..9ead7d0a2 100644 --- a/docs/keyboard-protocol.rst +++ b/docs/keyboard-protocol.rst @@ -629,7 +629,8 @@ specification. * No way to disambiguate :kbd:`Esc` keypresses, other than using 8-bit controls which are undesirable for other reasons * Incorrectly claims special keys are sometimes encoded using ``CSI letter`` encodings when it - is actually ``SS3 letter``. + is actually ``SS3 letter`` in all terminals newer than a VT-52, which is + pretty much everything. * :kbd:`ctrl+shift+tab`` should be ``CSI 9 ; 6 u`` not ``CSI 1 ; 5 Z`` (shift+tab is not a separate key from tab) * No support for the :kbd:`super` modifier. diff --git a/kittens/broadcast/main.py b/kittens/broadcast/main.py index ccc670ae8..e901d115e 100644 --- a/kittens/broadcast/main.py +++ b/kittens/broadcast/main.py @@ -9,7 +9,7 @@ from kitty.cli import parse_args from kitty.cli_stub import BroadcastCLIOptions -from kitty.key_encoding import RELEASE, encode_key_event, key_defs as K +from kitty.key_encoding import encode_key_event from kitty.rc.base import MATCH_TAB_OPTION, MATCH_WINDOW_OPTION from kitty.remote_control import create_basic_command, encode_send from kitty.typing import KeyEventType, ScreenSize @@ -59,15 +59,15 @@ def on_eot(self) -> None: def on_key(self, key_event: KeyEventType) -> None: if self.line_edit.on_key(key_event): self.commit_line() - if key_event.type is not RELEASE and not key_event.mods: - if key_event.key is K['ENTER']: - self.write_broadcast_text('\r') - self.print('') - self.line_edit.clear() - self.write(SAVE_CURSOR) - return + if key_event.matches('enter'): + self.write_broadcast_text('\r') + self.print('') + self.line_edit.clear() + self.write(SAVE_CURSOR) + return ek = encode_key_event(key_event) + ek = standard_b64encode(ek.encode('utf-8')).decode('ascii') self.write_broadcast_data('kitty-key:' + ek) def write_broadcast_text(self, text: str) -> None: diff --git a/kittens/diff/main.py b/kittens/diff/main.py index 114d8ede0..42fd8dff0 100644 --- a/kittens/diff/main.py +++ b/kittens/diff/main.py @@ -22,7 +22,7 @@ from kitty.conf.utils import KittensKeyAction from kitty.constants import appname from kitty.fast_data_types import wcswidth -from kitty.key_encoding import RELEASE, KeyEvent, enter_key, key_defs as K +from kitty.key_encoding import KeyEvent, EventType from kitty.options_stub import DiffOptions from kitty.utils import ScreenSize @@ -57,7 +57,6 @@ def highlight_collection(collection: 'Collection', aliases: Optional[Dict[str, s INITIALIZING, COLLECTED, DIFFED, COMMAND, MESSAGE = range(5) -ESCAPE = K['ESCAPE'] def generate_diff(collection: Collection, context: int) -> Union[str, Dict[str, Patch]]: @@ -483,48 +482,30 @@ def do_search(self) -> None: self.message = sanitize(_('No matches found')) self.cmd.bell() - def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: - if self.state is COMMAND: - self.line_edit.on_text(text, in_bracketed_paste) - self.draw_status_line() - return - if self.state is MESSAGE: - self.state = DIFFED - self.draw_status_line() - return - action = self.shortcut_action(text) - if action is not None: - return self.perform_action(action) - - def on_key(self, key_event: KeyEvent) -> None: - if self.state is MESSAGE: - if key_event.type is not RELEASE: + def on_key_event(self, key_event: KeyEvent, in_bracketed_paste: bool = False) -> None: + if key_event.text: + if self.state is COMMAND: + self.line_edit.on_text(key_event.text, in_bracketed_paste) + self.draw_status_line() + return + if self.state is MESSAGE: self.state = DIFFED self.draw_status_line() - return - if self.state is COMMAND: - if self.line_edit.on_key(key_event): - if not self.line_edit.current_input: + return + else: + if self.state is MESSAGE: + if key_event.type is not EventType.RELEASE: self.state = DIFFED - self.draw_status_line() + self.draw_status_line() return - if key_event.type is RELEASE: - return - if self.state is COMMAND: - if key_event.key is ESCAPE: - self.state = DIFFED - self.draw_status_line() + if self.state is COMMAND: + if self.line_edit.on_key(key_event): + if not self.line_edit.current_input: + self.state = DIFFED + self.draw_status_line() + return + if key_event.type is EventType.RELEASE: return - if key_event is enter_key: - self.state = DIFFED - self.do_search() - self.line_edit.clear() - self.draw_screen() - return - if self.state >= DIFFED and self.current_search is not None and key_event.key is ESCAPE: - self.current_search = None - self.draw_screen() - return action = self.shortcut_action(key_event) if action is not None: return self.perform_action(action) diff --git a/kittens/hints/main.py b/kittens/hints/main.py index 513d3d202..47befbb8d 100644 --- a/kittens/hints/main.py +++ b/kittens/hints/main.py @@ -17,9 +17,7 @@ from kitty.cli import parse_args from kitty.cli_stub import HintsCLIOptions from kitty.fast_data_types import set_clipboard_string -from kitty.key_encoding import ( - KeyEvent, backspace_key, enter_key, key_defs as K -) +from kitty.key_encoding import KeyEvent from kitty.typing import BossType, KittyCommonOpts from kitty.utils import ScreenSize, screen_size_function, set_primary_selection @@ -40,7 +38,6 @@ def kitty_common_opts() -> KittyCommonOpts: DEFAULT_HINT_ALPHABET = string.digits + string.ascii_lowercase DEFAULT_REGEX = r'(?m)^\s*(.+)\s*$' -ESCAPE = K['ESCAPE'] class Mark: @@ -177,11 +174,11 @@ def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: self.draw_screen() def on_key(self, key_event: KeyEvent) -> None: - if key_event is backspace_key: + if key_event.matches('backspace'): self.current_input = self.current_input[:-1] self.current_text = None self.draw_screen() - elif key_event is enter_key and self.current_input: + elif key_event.matches('enter') and self.current_input: try: idx = decode_hint(self.current_input, self.alphabet) self.chosen.append(self.index_map[idx]) @@ -196,7 +193,7 @@ def on_key(self, key_event: KeyEvent) -> None: self.draw_screen() else: self.quit_loop(0) - elif key_event.key is ESCAPE: + elif key_event.matches('esc'): self.quit_loop(0 if self.multiple else 1) def on_interrupt(self) -> None: @@ -288,6 +285,7 @@ def quotes(text: str, s: int, e: int) -> Tuple[int, int]: @postprocessor def ip(text: str, s: int, e: int) -> Tuple[int, int]: from ipaddress import ip_address + # Check validity of IPs (or raise InvalidMatch) ip = text[s:e] diff --git a/kittens/tui/handler.py b/kittens/tui/handler.py index 22f1fc488..1eb3fa7ac 100644 --- a/kittens/tui/handler.py +++ b/kittens/tui/handler.py @@ -8,6 +8,7 @@ Any, Callable, ContextManager, Dict, Optional, Sequence, Type, Union ) +from kitty.types import ParsedShortcut from kitty.typing import ( AbstractEventLoop, BossType, Debug, ImageManagerType, KeyEventType, KittensKeyActionType, LoopType, MouseEvent, ScreenSize, TermManagerType @@ -35,6 +36,7 @@ def _initialize( self.debug = debug self.cmd = commander(self) self._image_manager = image_manager + self._key_shortcuts: Dict[ParsedShortcut, KittensKeyActionType] = {} @property def image_manager(self) -> ImageManagerType: @@ -45,18 +47,16 @@ def image_manager(self) -> ImageManagerType: def asyncio_loop(self) -> AbstractEventLoop: return self._tui_loop.asycio_loop - def add_shortcut(self, action: KittensKeyActionType, key: str, mods: Optional[int] = None, is_text: Optional[bool] = False) -> None: - if not hasattr(self, '_text_shortcuts'): - self._text_shortcuts, self._key_shortcuts = {}, {} - if is_text: - self._text_shortcuts[key] = action - else: - self._key_shortcuts[(key, mods or 0)] = action + def add_shortcut(self, action: KittensKeyActionType, spec: Union[str, ParsedShortcut]) -> None: + if isinstance(spec, str): + from kitty.key_encoding import parse_shortcut + spec = parse_shortcut(spec) + self._key_shortcuts[spec] = action - def shortcut_action(self, key_event_or_text: Union[str, KeyEventType]) -> Optional[KittensKeyActionType]: - if isinstance(key_event_or_text, str): - return self._text_shortcuts.get(key_event_or_text) - return self._key_shortcuts.get((key_event_or_text.key, key_event_or_text.mods)) + def shortcut_action(self, key_event: KeyEventType) -> Optional[KittensKeyActionType]: + for sc, action in self._key_shortcuts.items(): + if key_event.matches(sc): + return action def __enter__(self) -> None: if self._image_manager is not None: @@ -85,6 +85,12 @@ def quit_loop(self, return_code: Optional[int] = None) -> None: def on_term(self) -> None: self._tui_loop.quit(1) + def on_key_event(self, key_event: KeyEventType, in_bracketed_paste: bool = False) -> None: + if key_event.text: + self.on_text(key_event.text, in_bracketed_paste) + else: + self.on_key(key_event) + def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: pass diff --git a/kittens/tui/line_edit.py b/kittens/tui/line_edit.py index 8f77a8ab1..4de7a7099 100644 --- a/kittens/tui/line_edit.py +++ b/kittens/tui/line_edit.py @@ -5,17 +5,10 @@ from typing import Callable, Tuple from kitty.fast_data_types import truncate_point_for_length, wcswidth -from kitty.key_encoding import RELEASE, KeyEvent, key_defs as K +from kitty.key_encoding import EventType, KeyEvent from .operations import RESTORE_CURSOR, SAVE_CURSOR, move_cursor_by -HOME = K['HOME'] -END = K['END'] -BACKSPACE = K['BACKSPACE'] -DELETE = K['DELETE'] -LEFT = K['LEFT'] -RIGHT = K['RIGHT'] - class LineEdit: @@ -137,22 +130,22 @@ def end(self) -> bool: return self.cursor_pos != orig def on_key(self, key_event: KeyEvent) -> bool: - if key_event.type is RELEASE: + if key_event.type is EventType.RELEASE: return False - elif key_event.key is HOME: + if key_event.matches('home'): return self.home() - elif key_event.key is END: + if key_event.matches('end'): return self.end() - elif key_event.key is BACKSPACE: + if key_event.matches('backspace'): self.backspace() return True - elif key_event.key is DELETE: + if key_event.matches('delete'): self.delete() return True - elif key_event.key is LEFT: + if key_event.matches('left'): self.left() return True - elif key_event.key is RIGHT: + if key_event.matches('right'): self.right() return True return False diff --git a/kittens/tui/loop.py b/kittens/tui/loop.py index 2f339791c..d27cb6b9a 100644 --- a/kittens/tui/loop.py +++ b/kittens/tui/loop.py @@ -21,7 +21,7 @@ ) from kitty.key_encoding import ( ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, backspace_key, decode_key_event, - enter_key, key_defs as K + enter_key ) from kitty.typing import ImageManagerType, KeyEventType, Protocol from kitty.utils import ScreenSizeGetter, screen_size_function, write_all @@ -29,8 +29,6 @@ from .handler import Handler from .operations import init_state, reset_state -C, D = K['C'], K['D'] - class BinaryWrite(Protocol): @@ -149,7 +147,7 @@ def initialize(self) -> None: self.write('Press the Enter key to quit') def on_key(self, key_event: KeyEventType) -> None: - if key_event is enter_key: + if key_event.key == 'ENTER': self.quit_loop(1) def on_interrupt(self) -> None: @@ -282,8 +280,23 @@ def _on_csi(self, csi: str) -> None: elif q == '~': if csi == '200~': self.in_bracketed_paste = True + return elif csi == '201~': self.in_bracketed_paste = False + return + elif q in 'u~ABCDHFPQRS': + try: + k = decode_key_event(csi[:-1], q) + except Exception: + pass + else: + if k.matches('ctrl+c'): + self.handler.on_interrupt() + return + if k.matches('ctrl+d'): + self.handler.on_eot() + return + self.handler.on_key_event(k) def _on_pm(self, pm: str) -> None: pass @@ -300,21 +313,7 @@ def _on_osc(self, osc: str) -> None: self.handler.on_clipboard_response(standard_b64decode(rest).decode('utf-8'), from_primary) def _on_apc(self, apc: str) -> None: - if apc.startswith('K'): - try: - k = decode_key_event(apc[1:]) - except Exception: - pass - else: - if k.mods is CTRL and k.type is not RELEASE: - if k.key is C: - self.handler.on_interrupt() - return - if k.key is D: - self.handler.on_eot() - return - self.handler.on_key(k) - elif apc.startswith('G'): + if apc.startswith('G'): if self.handler.image_manager is not None: self.handler.image_manager.handle_response(apc) # }}} diff --git a/kittens/tui/operations.py b/kittens/tui/operations.py index c8c91e624..a54356725 100644 --- a/kittens/tui/operations.py +++ b/kittens/tui/operations.py @@ -40,7 +40,6 @@ MOUSE_URXVT_MODE=(1015, '?'), ALTERNATE_SCREEN=(1049, '?'), BRACKETED_PASTE=(2004, '?'), - EXTENDED_KEYBOARD=(2017, '?'), ) F = TypeVar('F') @@ -270,8 +269,8 @@ def init_state(alternate_screen: bool = True) -> str: reset_mode('MOUSE_MOTION_TRACKING') + reset_mode('MOUSE_MOVE_TRACKING') + reset_mode('FOCUS_TRACKING') + reset_mode('MOUSE_UTF8_MODE') + reset_mode('MOUSE_SGR_MODE') + reset_mode('MOUSE_UTF8_MODE') + - set_mode('BRACKETED_PASTE') + set_mode('EXTENDED_KEYBOARD') + - SAVE_COLORS + + set_mode('BRACKETED_PASTE') + SAVE_COLORS + + '\033[>31u' + # extended keyboard mode '\033[*x' # reset DECSACE to default region select ) if alternate_screen: @@ -287,6 +286,7 @@ def reset_state(normal_screen: bool = True) -> str: ans += RESTORE_PRIVATE_MODE_VALUES ans += RESTORE_CURSOR ans += RESTORE_COLORS + ans += '\033[ None: - from .fast_data_types import glfw_get_key_name, GLFW_MOD_ALT, GLFW_MOD_SHIFT, GLFW_MOD_CONTROL, GLFW_MOD_SUPER + from .fast_data_types import ( + GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT, GLFW_MOD_SUPER, + glfw_get_key_name + ) mmap = {'shift': GLFW_MOD_SHIFT, 'alt': GLFW_MOD_ALT, 'ctrl': GLFW_MOD_CONTROL, ('cmd' if is_macos else 'super'): GLFW_MOD_SUPER} keys = [] for key_spec in key_sequence: diff --git a/kitty/conf/utils.py b/kitty/conf/utils.py index 2996fa60f..2f1177889 100644 --- a/kitty/conf/utils.py +++ b/kitty/conf/utils.py @@ -11,7 +11,8 @@ ) from ..rgb import Color, to_color as as_color -from ..utils import log_error, expandvars +from ..types import ParsedShortcut +from ..utils import expandvars, log_error key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$') T = TypeVar('T') @@ -301,38 +302,6 @@ def w(f: Callable) -> Callable: return func_with_args, ans -def parse_kittens_shortcut(sc: str) -> Tuple[Optional[int], str, bool]: - from ..key_encoding import config_key_map, config_mod_map, text_match - if sc.endswith('+'): - parts = list(filter(None, sc.rstrip('+').split('+') + ['+'])) - else: - parts = sc.split('+') - qmods = parts[:-1] - if qmods: - resolved_mods = 0 - for mod in qmods: - m = config_mod_map.get(mod.upper()) - if m is None: - raise ValueError('Unknown shortcut modifiers: {}'.format(sc)) - resolved_mods |= m - mods: Optional[int] = resolved_mods - else: - mods = None - is_text = False - rkey = parts[-1] - tkey = text_match(rkey) - if tkey is None: - rkey = rkey.upper() - q = config_key_map.get(rkey) - if q is None: - raise ValueError('Unknown shortcut key: {}'.format(sc)) - rkey = q - else: - is_text = True - rkey = tkey - return mods, rkey, is_text - - KittensKeyAction = Tuple[str, Tuple[str, ...]] @@ -362,15 +331,12 @@ def parse_kittens_func_args(action: str, args_funcs: Dict[str, Callable]) -> Kit return func, tuple(args) -KittensKey = Tuple[str, Optional[int], bool] - - def parse_kittens_key( val: str, funcs_with_args: Dict[str, Callable] -) -> Optional[Tuple[KittensKeyAction, KittensKey]]: +) -> Optional[Tuple[KittensKeyAction, ParsedShortcut]]: + from ..key_encoding import parse_shortcut sc, action = val.partition(' ')[::2] if not sc or not action: return None - mods, key, is_text = parse_kittens_shortcut(sc) ans = parse_kittens_func_args(action, funcs_with_args) - return ans, (key, mods, is_text) + return ans, parse_shortcut(sc) diff --git a/kitty/config.py b/kitty/config.py index a53da8ba7..0a19473a5 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -20,9 +20,10 @@ parse_config_base, python_string, to_bool, to_cmdline ) from .config_data import InvalidMods, all_options, parse_shortcut, type_convert -from .constants import SingleKey, cache_dir, defconf, is_macos +from .constants import cache_dir, defconf, is_macos from .fonts import FontFeature from .options_stub import Options as OptionsStub +from .types import SingleKey from .typing import TypedDict from .utils import expandvars, log_error diff --git a/kitty/config_data.py b/kitty/config_data.py index 8a620b91f..c2cfb3ad8 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -16,13 +16,15 @@ choices, positive_float, positive_int, to_bool, to_cmdline as tc, to_color, to_color_or_none, unit_float ) -from .constants import ( - FloatEdges, SingleKey, config_dir, is_macos -) +from .constants import config_dir, is_macos from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE -from .key_names import get_key_name_lookup, key_name_aliases +from .key_names import ( + character_key_name_aliases, functional_key_name_aliases, + get_key_name_lookup +) from .layout.interface import all_layouts from .rgb import Color, color_as_int, color_as_sharp, color_from_int +from .types import FloatEdges, SingleKey from .utils import log_error @@ -60,6 +62,8 @@ def to_modifiers(val: str) -> int: def parse_shortcut(sc: str) -> SingleKey: + if sc.endswith('+') and len(sc) > 1: + sc = sc[:-1] + 'plus' parts = sc.split('+') mods = 0 if len(parts) > 1: @@ -67,6 +71,7 @@ def parse_shortcut(sc: str) -> SingleKey: if not mods: raise InvalidMods('Invalid shortcut') q = parts[-1] + q = character_key_name_aliases.get(q.upper(), q) is_native = False if q.startswith('0x'): try: @@ -80,7 +85,7 @@ def parse_shortcut(sc: str) -> SingleKey: key = ord(q) except Exception: uq = q.upper() - uq = key_name_aliases.get(uq, uq) + uq = functional_key_name_aliases.get(uq, uq) x: Optional[int] = getattr(defines, f'GLFW_FKEY_{uq}', None) if x is None: lf = get_key_name_lookup() diff --git a/kitty/constants.py b/kitty/constants.py index c370f830e..a77da328d 100644 --- a/kitty/constants.py +++ b/kitty/constants.py @@ -27,45 +27,6 @@ class Version(NamedTuple): base = os.path.dirname(os.path.abspath(__file__)) -class Edges(NamedTuple): - left: int = 0 - top: int = 0 - right: int = 0 - bottom: int = 0 - - -class FloatEdges(NamedTuple): - left: float = 0 - top: float = 0 - right: float = 0 - bottom: float = 0 - - -class ScreenGeometry(NamedTuple): - xstart: float - ystart: float - xnum: int - ynum: int - dx: float - dy: float - - -class WindowGeometry(NamedTuple): - left: int - top: int - right: int - bottom: int - xnum: int - ynum: int - spaces: Edges = Edges() - - -class SingleKey(NamedTuple): - mods: int = 0 - is_native: bool = False - key: int = -1 - - @lru_cache(maxsize=2) def kitty_exe() -> str: rpath = sys._xoptions.get('bundle_exe_dir') diff --git a/kitty/key_encoding.py b/kitty/key_encoding.py index 7c7eb7283..ec04691cd 100644 --- a/kitty/key_encoding.py +++ b/kitty/key_encoding.py @@ -2,476 +2,254 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2017, Kovid Goyal -import string -from typing import Dict, NamedTuple, Optional +from enum import IntEnum +from functools import lru_cache +from typing import Dict, NamedTuple, Optional, Tuple, Union from . import fast_data_types as defines -from .key_names import key_name_aliases +from .fast_data_types import KeyEvent as WindowSystemKeyEvent +from .key_names import character_key_name_aliases, functional_key_name_aliases +from .types import ParsedShortcut -# ENCODING {{{ -ENCODING = { - '0': 'G', - '1': 'H', - '2': 'I', - '3': 'J', - '4': 'K', - '5': 'L', - '6': 'M', - '7': 'N', - '8': 'O', - '9': 'P', - 'A': 'S', - 'APOSTROPHE': 'B', - 'B': 'T', - 'BACKSLASH': 't', - 'BACKSPACE': '1', - 'C': 'U', - 'CAPS LOCK': ':', - 'COMMA': 'C', - 'D': 'V', - 'DELETE': '3', - 'DOWN': '6', - 'E': 'W', - 'END': '-', - 'ENTER': 'z', - 'EQUAL': 'R', - 'ESCAPE': 'y', - 'F': 'X', - 'F1': '/', - 'F10': ']', - 'F11': '{', - 'F12': '}', - 'F13': '@', - 'F14': '%', - 'F15': '$', - 'F16': '#', - 'F17': 'BA', - 'F18': 'BB', - 'F19': 'BC', - 'F2': '*', - 'F20': 'BD', - 'F21': 'BE', - 'F22': 'BF', - 'F23': 'BG', - 'F24': 'BH', - 'F25': 'BI', - 'F3': '?', - 'F4': '&', - 'F5': '<', - 'F6': '>', - 'F7': '(', - 'F8': ')', - 'F9': '[', - 'G': 'Y', - 'GRAVE ACCENT': 'v', - 'H': 'Z', - 'HOME': '.', - 'I': 'a', - 'INSERT': '2', - 'J': 'b', - 'K': 'c', - 'KP 0': 'BJ', - 'KP 1': 'BK', - 'KP 2': 'BL', - 'KP 3': 'BM', - 'KP 4': 'BN', - 'KP 5': 'BO', - 'KP 6': 'BP', - 'KP 7': 'BQ', - 'KP 8': 'BR', - 'KP 9': 'BS', - 'KP ADD': 'BX', - 'KP DECIMAL': 'BT', - 'KP DIVIDE': 'BU', - 'KP ENTER': 'BY', - 'KP EQUAL': 'BZ', - 'KP MULTIPLY': 'BV', - 'KP SUBTRACT': 'BW', - 'L': 'd', - 'LEFT': '5', - 'LEFT ALT': 'Bc', - 'LEFT BRACKET': 's', - 'LEFT CONTROL': 'Bb', - 'LEFT SHIFT': 'Ba', - 'LEFT SUPER': 'Bd', - 'M': 'e', - 'MINUS': 'D', - 'N': 'f', - 'NUM LOCK': '=', - 'O': 'g', - 'P': 'h', - 'PAGE DOWN': '9', - 'PAGE UP': '8', - 'PAUSE': '!', - 'PERIOD': 'E', - 'PRINT SCREEN': '^', - 'Q': 'i', - 'R': 'j', - 'RIGHT': '4', - 'RIGHT ALT': 'Bg', - 'RIGHT BRACKET': 'u', - 'RIGHT CONTROL': 'Bf', - 'RIGHT SHIFT': 'Be', - 'RIGHT SUPER': 'Bh', - 'S': 'k', - 'SCROLL LOCK': '+', - 'SEMICOLON': 'Q', - 'SLASH': 'F', - 'SPACE': 'A', - 'T': 'l', - 'TAB': '0', - 'U': 'm', - 'UP': '7', - 'V': 'n', - 'W': 'o', - 'WORLD 1': 'w', - 'WORLD 2': 'x', - 'X': 'p', - 'Y': 'q', - 'Z': 'r', - 'PLUS': 'Bi', - 'UNDERSCORE': 'Bj', - 'MENU': 'Bk', - 'EXCLAM': 'Bl', - 'DOUBLE QUOTE': 'Bm', - 'NUMBER SIGN': 'Bn', - 'DOLLAR': 'Bo', - 'AMPERSAND': 'Bp', - 'PARENTHESIS LEFT': 'Bq', - 'PARENTHESIS RIGHT': 'Br', - 'COLON': 'Bs', - 'LESS': 'Bt', - 'GREATER': 'Bu', - 'AT': 'Bv', - 'PARAGRAPH': 'Bw', - 'MASCULINE': 'Bx', - 'A GRAVE': 'By', - 'A DIAERESIS': 'Bz', - 'A RING': 'B0', - 'AE': 'B1', - 'C CEDILLA': 'B2', - 'E GRAVE': 'B3', - 'E ACUTE': 'B4', - 'I GRAVE': 'B5', - 'N TILDE': 'B6', - 'O GRAVE': 'B7', - 'O DIAERESIS': 'B8', - 'O SLASH': 'B9', - 'U GRAVE': 'B.', - 'U DIAERESIS': 'B-', - 'S SHARP': 'B:', - 'CYRILLIC A': 'B+', - 'CYRILLIC BE': 'B=', - 'CYRILLIC VE': 'B^', - 'CYRILLIC GHE': 'B!', - 'CYRILLIC DE': 'B/', - 'CYRILLIC IE': 'B*', - 'CYRILLIC ZHE': 'B?', - 'CYRILLIC ZE': 'B&', - 'CYRILLIC I': 'B<', - 'CYRILLIC SHORT I': 'B>', - 'CYRILLIC KA': 'B(', - 'CYRILLIC EL': 'B)', - 'CYRILLIC EM': 'B[', - 'CYRILLIC EN': 'B]', - 'CYRILLIC O': 'B{', - 'CYRILLIC PE': 'B}', - 'CYRILLIC ER': 'B@', - 'CYRILLIC ES': 'B%', - 'CYRILLIC TE': 'B$', - 'CYRILLIC U': 'B#', - 'CYRILLIC EF': 'CA', - 'CYRILLIC HA': 'CB', - 'CYRILLIC TSE': 'CC', - 'CYRILLIC CHE': 'CD', - 'CYRILLIC SHA': 'CE', - 'CYRILLIC SHCHA': 'CF', - 'CYRILLIC HARD SIGN': 'CG', - 'CYRILLIC YERU': 'CH', - 'CYRILLIC SOFT SIGN': 'CI', - 'CYRILLIC E': 'CJ', - 'CYRILLIC YU': 'CK', - 'CYRILLIC YA': 'CL', - 'CYRILLIC IO': 'CM', - 'CIRCUMFLEX': 'CN' -} -KEY_MAP = { - 32: 'A', - 33: 'Bl', - 34: 'Bm', - 35: 'Bn', - 36: 'Bo', - 38: 'Bp', - 39: 'B', - 40: 'Bq', - 41: 'Br', - 43: 'Bi', - 44: 'C', - 45: 'D', - 46: 'E', - 47: 'F', - 48: 'G', - 49: 'H', - 50: 'I', - 51: 'J', - 52: 'K', - 53: 'L', - 54: 'M', - 55: 'N', - 56: 'O', - 57: 'P', - 58: 'Bs', - 59: 'Q', - 60: 'Bt', - 61: 'R', - 62: 'Bu', - 64: 'Bv', - 65: 'S', - 66: 'T', - 67: 'U', - 68: 'V', - 69: 'W', - 70: 'X', - 71: 'Y', - 72: 'Z', - 73: 'a', - 74: 'b', - 75: 'c', - 76: 'd', - 77: 'e', - 78: 'f', - 79: 'g', - 80: 'h', - 81: 'i', - 82: 'j', - 83: 'k', - 84: 'l', - 85: 'm', - 86: 'n', - 87: 'o', - 88: 'p', - 89: 'q', - 90: 'r', - 91: 's', - 92: 't', - 93: 'u', - 94: 'CN', - 95: 'Bj', - 96: 'v', - 161: 'w', - 162: 'x', - 167: 'Bw', - 186: 'Bx', - 192: 'By', - 196: 'Bz', - 197: 'B0', - 198: 'B1', - 199: 'B2', - 200: 'B3', - 201: 'B4', - 204: 'B5', - 209: 'B6', - 210: 'B7', - 214: 'B8', - 216: 'B9', - 217: 'B.', - 220: 'B-', - 222: 'B:', - 223: 'B+', - 224: 'B=', - 225: 'B^', - 226: 'B!', - 227: 'B/', - 228: 'B*', - 229: 'B?', - 230: 'B&', - 231: 'B<', - 232: 'B>', - 233: 'B(', - 234: 'B)', - 235: 'B[', - 236: 'B]', - 237: 'B{', - 238: 'B}', - 239: 'B@', - 240: 'B%', - 241: 'B$', - 242: 'B#', - 243: 'CA', - 244: 'CB', - 245: 'CC', - 246: 'CD', - 247: 'CE', - 248: 'CF', - 249: 'CG', - 250: 'CH', - 251: 'CI', - 252: 'CJ', - 253: 'CK', - 254: 'CL', - 255: 'CM', - 256: 'y', - 257: 'z', - 258: '0', - 259: '1', - 260: '2', - 261: '3', - 262: '4', - 263: '5', - 264: '6', - 265: '7', - 266: '8', - 267: '9', - 268: '.', - 269: '-', - 280: ':', - 281: '+', - 282: '=', - 283: '^', - 284: '!', - 290: '/', - 291: '*', - 292: '?', - 293: '&', - 294: '<', - 295: '>', - 296: '(', - 297: ')', - 298: '[', - 299: ']', - 300: '{', - 301: '}', - 302: '@', - 303: '%', - 304: '$', - 305: '#', - 306: 'BA', - 307: 'BB', - 308: 'BC', - 309: 'BD', - 310: 'BE', - 311: 'BF', - 312: 'BG', - 313: 'BH', - 314: 'BI', - 320: 'BJ', - 321: 'BK', - 322: 'BL', - 323: 'BM', - 324: 'BN', - 325: 'BO', - 326: 'BP', - 327: 'BQ', - 328: 'BR', - 329: 'BS', - 330: 'BT', - 331: 'BU', - 332: 'BV', - 333: 'BW', - 334: 'BX', - 335: 'BY', - 336: 'BZ', - 340: 'Ba', - 341: 'Bb', - 342: 'Bc', - 343: 'Bd', - 344: 'Be', - 345: 'Bf', - 346: 'Bg', - 347: 'Bh', - 348: 'Bk' -} -# END_ENCODING }}} - -text_keys = ( - string.ascii_uppercase + string.ascii_lowercase + string.digits + - '`~!@#$%^&*()_-+=[{]}\\|<,>./?;:\'" ' - 'ÄäÖöÜüߧºàåæçèéìñòøùабвгдежзийклмнопрстуфхцчшщъыьэюяё' -) +# 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: 'LEFT_SHIFT', + 57428: 'LEFT_CONTROL', + 57429: 'LEFT_ALT', + 57430: 'LEFT_SUPER', + 57431: 'RIGHT_SHIFT', + 57432: 'RIGHT_CONTROL', + 57433: 'RIGHT_ALT', + 57434: 'RIGHT_SUPER', + 57435: 'MEDIA_PLAY', + 57436: 'MEDIA_PAUSE', + 57437: 'MEDIA_PLAY_PAUSE', + 57438: 'MEDIA_REVERSE', + 57439: 'MEDIA_STOP', + 57440: 'MEDIA_FAST_FORWARD', + 57441: 'MEDIA_REWIND', + 57442: 'MEDIA_TRACK_NEXT', + 57443: 'MEDIA_TRACK_PREVIOUS', + 57444: 'MEDIA_RECORD', + 57445: 'LOWER_VOLUME', + 57446: 'RAISE_VOLUME', + 57447: 'MUTE_VOLUME'} +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, 'F': 8, 'H': 7, 'P': 11, 'Q': 12, 'R': 13, 'S': 14} +tilde_trailers = {57348, 57349, 57354, 57355, 57368, 57369, 57370, 57371, 57372, 57373, 57374, 57375} +# end csi mapping +# }}} -def text_match(key: str) -> Optional[str]: - if key.upper() == 'SPACE': - return ' ' - if key not in text_keys: - return None - return key +@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()} -def encode( - integer: int, - chars: str = string.ascii_uppercase + string.ascii_lowercase + string.digits + - '.-:+=^!/*?&<>()[]{}@%$#' -) -> str: - ans = '' - d = len(chars) - while True: - integer, remainder = divmod(integer, d) - ans = chars[remainder] + ans - if integer == 0: - break - return ans +@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()} -def symbolic_name(glfw_name: str) -> str: - return glfw_name[9:].replace('_', ' ') - - -def glfw_key_name(symbolic_name: str) -> str: - return 'GLFW_KEY_' + symbolic_name.replace(' ', '_') - - -def update_encoding() -> None: - import re - import subprocess - keys = {a for a in dir(defines) if a.startswith('GLFW_KEY_')} - ans = ENCODING - key_map = {} - i = len(ans) - for k in sorted(keys, key=lambda k: int(getattr(defines, k))): - if k in ('GLFW_KEY_LAST', 'GLFW_KEY_LAST_PRINTABLE'): - continue - val = getattr(defines, k) - name = symbolic_name(k) - if val <= defines.GLFW_KEY_LAST and val != defines.GLFW_KEY_UNKNOWN: - if name not in ans: - ans[name] = encode(i) - i += 1 - key_map[val] = ans[name] - with open(__file__, 'r+') as f: - raw = f.read() - nraw = re.sub( - r'^ENCODING = {.+^# END_ENCODING', - 'ENCODING = {!r}\nKEY_MAP={!r}\n# END_ENCODING'.format( - ans, key_map - ), - raw, - flags=re.MULTILINE | re.DOTALL - ) - if raw == nraw: - raise SystemExit('Failed to replace ENCODING dict') - f.seek(0), f.truncate() - f.write(nraw) - subprocess.check_call(['yapf', '-i', __file__]) - - -class KeyEvent(NamedTuple): - type: int - mods: int - key: str +@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('+') and len(spec) > 1: + spec = 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) + mods = tuple(config_mod_map.get(x.upper(), SUPER << 8) for x in parts[:-1]) + mod_val = 0 + 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 + + def matches(self, spec: Union[str, ParsedShortcut], types: int = EventType.PRESS | EventType.REPEAT) -> bool: + if not self.type & types: + return False + q = self.mods + is_shifted = bool(self.shifted_key and self.shift) + if is_shifted: + q = self.mods & ~SHIFT + kq = self.shifted_key + else: + kq = self.key + if isinstance(spec, str): + spec = parse_shortcut(spec) + if q != spec.mods: + return False + return kq == spec.key_name + + 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 + + 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 = 1, 2, 4, 8 -type_map = {'p': PRESS, 't': REPEAT, 'r': RELEASE} -rtype_map = {v: k for k, v in type_map.items()} -mod_map = {c: i for i, c in enumerate('ABCDEFGHIJKLMNOP')} -rmod_map = {v: k for k, v in mod_map.items()} -key_rmap = {} -key_defs: Dict[str, str] = {} -config_key_map = {} +enter_key = KeyEvent(key='ENTER') +backspace_key = KeyEvent(key='BACKSPACE') config_mod_map = { 'SHIFT': SHIFT, 'ALT': ALT, @@ -483,56 +261,102 @@ class KeyEvent(NamedTuple): 'CTRL': CTRL, 'CONTROL': CTRL } -for key_name, enc in ENCODING.items(): - key_name = key_name.replace(' ', '_') - key_defs[key_name] = config_key_map[key_name] = key_name - key_rmap[enc] = key_name -config_key_map.update({k: key_defs[v] for k, v in key_name_aliases.items() if v in key_defs}) - -enter_key = KeyEvent(PRESS, 0, key_defs['ENTER']) -backspace_key = KeyEvent(PRESS, 0, key_defs['BACKSPACE']) -globals().update(key_defs) -del key_name, enc -def decode_key_event(text: str) -> KeyEvent: - typ = type_map[text[0]] - mods = mod_map[text[1]] - key = key_rmap[text[2:4]] - return KeyEvent(typ, mods, key) +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 'ABCDHFPQRS': + keynum = letter_trailer_to_csi_number_map[csi_type] + + def key_name(num: int) -> str: + if not num: + return '' + num = csi_number_to_functional_number_map.get(num, num) + ans = functional_key_number_to_name_map.get(num) + 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), + 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 + 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: - typ = rtype_map[key_event.type] - mods = rmod_map[key_event.mods] - key = ENCODING[key_event.key.replace('_', ' ')] - return typ + mods + key - - -class WindowSystemKeyEvent(NamedTuple): - code: int - mods: int - action: int + 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() + 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 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) -> Optional[WindowSystemKeyEvent]: - k = decode_key_event(text) - glfw_name = glfw_key_name(k.key) - glfw_code = getattr(defines, glfw_name, None) - if glfw_code is None: + csi, trailer = text[2:-1], text[-1] + try: + k = decode_key_event(csi, trailer) + except Exception: return None - action = defines.GLFW_PRESS - if k.type is RELEASE: - action = defines.GLFW_RELEASE - elif k.type is REPEAT: - action = defines.GLFW_REPEAT - mods = 0 - if k.mods & CTRL: - mods |= defines.GLFW_MOD_CONTROL - if k.mods & ALT: - mods |= defines.GLFW_MOD_ALT - if k.mods & SUPER: - mods |= defines.GLFW_MOD_SUPER - if k.mods & SHIFT: - mods |= defines.GLFW_MOD_SHIFT - return WindowSystemKeyEvent(glfw_code, mods, action) + return k.as_window_system_event() diff --git a/kitty/key_names.py b/kitty/key_names.py index ad2115186..9c7c5ae6b 100644 --- a/kitty/key_names.py +++ b/kitty/key_names.py @@ -9,9 +9,7 @@ from .constants import is_macos -key_name_aliases = { - 'SPACE': ' ', - 'SPC': ' ', +functional_key_name_aliases = { 'ESC': 'ESCAPE', 'PGUP': 'PAGE_UP', 'PAGEUP': 'PAGE_UP', @@ -21,7 +19,17 @@ 'ARROWUP': 'UP', 'ARROWDOWN': 'DOWN', 'ARROWRIGHT': 'RIGHT', - 'ARROWLEFT': 'LEFT' + 'ARROWLEFT': 'LEFT', + 'DEL': 'DELETE', +} + + +character_key_name_aliases = { + 'SPC': ' ', + 'SPACE': ' ', + 'PLUS': '+', + 'MINUS': '-', + 'HYPHEN': '-', } LookupFunc = Callable[[str, bool], Optional[int]] diff --git a/kitty/keys.py b/kitty/keys.py index 7703594c3..81e77a431 100644 --- a/kitty/keys.py +++ b/kitty/keys.py @@ -5,8 +5,8 @@ from typing import Optional, Union from .config import KeyAction, KeyMap, SequenceMap, SubSequenceMap -from .constants import SingleKey from .fast_data_types import KeyEvent +from .types import SingleKey from .typing import ScreenType diff --git a/kitty/layout/base.py b/kitty/layout/base.py index 29104b392..b5226caff 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -9,14 +9,14 @@ Tuple ) -from kitty.constants import Edges, WindowGeometry +from kitty.borders import BorderColor from kitty.fast_data_types import ( Region, set_active_window, viewport_for_window ) from kitty.options_stub import Options +from kitty.types import Edges, WindowGeometry from kitty.typing import TypedDict, WindowType from kitty.window_list import WindowGroup, WindowList -from kitty.borders import BorderColor class BorderLine(NamedTuple): diff --git a/kitty/layout/grid.py b/kitty/layout/grid.py index 62df0df54..b1d0447ab 100644 --- a/kitty/layout/grid.py +++ b/kitty/layout/grid.py @@ -10,7 +10,7 @@ ) from kitty.borders import BorderColor -from kitty.constants import Edges +from kitty.types import Edges from kitty.typing import WindowType from kitty.window_list import WindowGroup, WindowList diff --git a/kitty/layout/splits.py b/kitty/layout/splits.py index afb1aa62b..c7a8245fe 100644 --- a/kitty/layout/splits.py +++ b/kitty/layout/splits.py @@ -8,7 +8,7 @@ ) from kitty.borders import BorderColor -from kitty.constants import Edges, WindowGeometry +from kitty.types import Edges, WindowGeometry from kitty.typing import EdgeLiteral, WindowType from kitty.window_list import WindowGroup, WindowList diff --git a/kitty/layout/tall.py b/kitty/layout/tall.py index d59c23420..0e5346251 100644 --- a/kitty/layout/tall.py +++ b/kitty/layout/tall.py @@ -7,7 +7,7 @@ from kitty.borders import BorderColor from kitty.conf.utils import to_bool -from kitty.constants import Edges +from kitty.types import Edges from kitty.typing import EdgeLiteral, WindowType from kitty.window_list import WindowGroup, WindowList diff --git a/kitty/layout/vertical.py b/kitty/layout/vertical.py index ce253179e..ced52e85d 100644 --- a/kitty/layout/vertical.py +++ b/kitty/layout/vertical.py @@ -5,7 +5,7 @@ from typing import Dict, Generator, Iterable, List, Tuple from kitty.borders import BorderColor -from kitty.constants import Edges +from kitty.types import Edges from kitty.typing import WindowType from kitty.window_list import WindowGroup, WindowList diff --git a/kitty/main.py b/kitty/main.py index 2683f95dc..efd2081a4 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -17,7 +17,7 @@ from .conf.utils import BadLine from .config import cached_values_for from .constants import ( - SingleKey, appname, beam_cursor_data_file, config_dir, glfw_path, is_macos, + appname, beam_cursor_data_file, config_dir, glfw_path, is_macos, is_wayland, kitty_exe, logo_data_file, running_in_kitty ) from .fast_data_types import ( @@ -30,6 +30,7 @@ from .options_stub import Options as OptionsStub from .os_window_size import initial_window_size_func from .session import get_os_window_sizing_data +from .types import SingleKey from .utils import ( detach, expandvars, find_exe, log_error, read_shell_environment, single_instance, startup_notification_handler, unix_socket_paths diff --git a/kitty/options_stub.py b/kitty/options_stub.py index fcf43eb66..0c4e7ae62 100644 --- a/kitty/options_stub.py +++ b/kitty/options_stub.py @@ -34,10 +34,11 @@ def generate_stub(): all_options, class_name='DiffOptions', preamble_lines=( - 'from kitty.conf.utils import KittensKey, KittensKeyAction', + 'from kitty.conf.utils import KittensKeyAction', + 'from kitty.types import ParsedShortcut', ), extra_fields=( - ('key_definitions', 'typing.Dict[KittensKey, KittensKeyAction]'), + ('key_definitions', 'typing.Dict[ParsedShortcut, KittensKeyAction]'), ) ) diff --git a/kitty/os_window_size.py b/kitty/os_window_size.py index afa10e6f5..ada1575f9 100644 --- a/kitty/os_window_size.py +++ b/kitty/os_window_size.py @@ -4,7 +4,8 @@ from typing import Any, Callable, Dict, NamedTuple, Tuple -from .constants import FloatEdges, is_macos +from .constants import is_macos +from .types import FloatEdges from .typing import EdgeLiteral from .utils import log_error diff --git a/kitty/rc/send_text.py b/kitty/rc/send_text.py index 7cb82a88f..ae99088bb 100644 --- a/kitty/rc/send_text.py +++ b/kitty/rc/send_text.py @@ -8,9 +8,9 @@ from typing import TYPE_CHECKING, Dict, Generator, List, Optional from kitty.config import parse_send_text_bytes -from kitty.key_encoding import ( - WindowSystemKeyEvent, decode_key_event_as_window_system_key -) +from kitty.key_encoding import decode_key_event_as_window_system_key +from kitty.fast_data_types import KeyEvent as WindowSystemKeyEvent + from .base import ( MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType, @@ -145,7 +145,8 @@ def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: elif encoding == 'base64': data = base64.standard_b64decode(q) elif encoding == 'kitty-key': - data = decode_key_event_as_window_system_key(q) + data = base64.standard_b64decode(q) + data = decode_key_event_as_window_system_key(data) else: raise TypeError(f'Invalid encoding for send-text data: {encoding}') exclude_active = payload_get('exclude_active') @@ -153,7 +154,7 @@ def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: if window is not None: if not exclude_active or window is not boss.active_window: if isinstance(data, WindowSystemKeyEvent): - kdata = window.encoded_key(data.code, mods=data.mods, action=data.action) + kdata = window.encoded_key(data) if kdata: window.write_to_child(kdata) else: diff --git a/kitty/tab_bar.py b/kitty/tab_bar.py index 0fdab9e46..38ef5be41 100644 --- a/kitty/tab_bar.py +++ b/kitty/tab_bar.py @@ -6,7 +6,7 @@ from typing import Any, Dict, NamedTuple, Optional, Sequence, Tuple from .config import build_ansi_color_table -from .constants import WindowGeometry +from .types import WindowGeometry from .fast_data_types import ( DECAWM, Screen, cell_size_for_window, pt_to_px, set_tab_bar_render_data, viewport_for_window diff --git a/kitty/types.py b/kitty/types.py new file mode 100644 index 000000000..beb4316db --- /dev/null +++ b/kitty/types.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2021, Kovid Goyal + +from typing import NamedTuple + + +class ParsedShortcut(NamedTuple): + mods: int + key_name: str + + +class Edges(NamedTuple): + left: int = 0 + top: int = 0 + right: int = 0 + bottom: int = 0 + + +class FloatEdges(NamedTuple): + left: float = 0 + top: float = 0 + right: float = 0 + bottom: float = 0 + + +class ScreenGeometry(NamedTuple): + xstart: float + ystart: float + xnum: int + ynum: int + dx: float + dy: float + + +class WindowGeometry(NamedTuple): + left: int + top: int + right: int + bottom: int + xnum: int + ynum: int + spaces: Edges = Edges() + + +class SingleKey(NamedTuple): + mods: int = 0 + is_native: bool = False + key: int = -1 diff --git a/kitty/window.py b/kitty/window.py index 09d720fe8..94c35a6c2 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -19,13 +19,13 @@ from .child import ProcessDesc from .cli_stub import CLIOptions from .config import build_ansi_color_table -from .constants import ScreenGeometry, WindowGeometry, appname, wakeup +from .constants import appname, wakeup from .fast_data_types import ( BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM, CELL_PROGRAM, CELL_SPECIAL_PROGRAM, DCS, DECORATION, DIM, GLFW_MOD_CONTROL, - GLFW_PRESS, GRAPHICS_ALPHA_MASK_PROGRAM, GRAPHICS_PREMULT_PROGRAM, - GRAPHICS_PROGRAM, MARK, MARK_MASK, OSC, REVERSE, SCROLL_FULL, SCROLL_LINE, - SCROLL_PAGE, STRIKETHROUGH, TINT_PROGRAM, Screen, add_timer, add_window, + GRAPHICS_ALPHA_MASK_PROGRAM, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_PROGRAM, + MARK, MARK_MASK, OSC, REVERSE, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, + STRIKETHROUGH, TINT_PROGRAM, KeyEvent, Screen, add_timer, add_window, cell_size_for_window, compile_program, encode_key_for_tty, get_boss, get_clipboard_string, init_cell_program, pt_to_px, set_clipboard_string, set_titlebar_color, set_window_padding, set_window_render_data, @@ -36,6 +36,7 @@ from .options_stub import Options from .rgb import to_color from .terminfo import get_capabilities +from .types import ScreenGeometry, WindowGeometry from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict from .utils import ( color_as_int, get_primary_selection, load_shaders, open_cmd, open_url, @@ -881,18 +882,20 @@ def copy_to_clipboard(self) -> None: if text: set_clipboard_string(text) - def encoded_key(self, key: int, mods: int = 0, action: int = GLFW_PRESS) -> bytes: + def encoded_key(self, key_event: KeyEvent) -> bytes: return encode_key_for_tty( - key=key, mods=mods, key_encoding_flags=self.screen.current_key_encoding_flags(), - cursor_key_mode=self.screen.cursor_key_mode, action=action - ).encode('ascii') + key=key_event.key, shifted_key=key_event.shifted_key, alternate_key=key_event.alternate_key, + mods=key_event.mods, action=key_event.action, text=key_event.text, + key_encoding_flags=self.screen.current_key_encoding_flags(), + cursor_key_mode=self.screen.cursor_key_mode, + ).encode('ascii') def copy_or_interrupt(self) -> None: text = self.text_for_selection() if text: set_clipboard_string(text) else: - self.write_to_child(self.encoded_key(ord('c'), mods=GLFW_MOD_CONTROL)) + self.write_to_child(self.encoded_key(KeyEvent(key=ord('c'), mods=GLFW_MOD_CONTROL))) def copy_and_clear_or_interrupt(self) -> None: self.copy_or_interrupt() diff --git a/kitty/window_list.py b/kitty/window_list.py index 3abc853d9..79e025187 100644 --- a/kitty/window_list.py +++ b/kitty/window_list.py @@ -10,7 +10,7 @@ Any, Deque, Dict, Generator, Iterator, List, Optional, Tuple, Union ) -from .constants import WindowGeometry +from .types import WindowGeometry from .typing import EdgeLiteral, TabType, WindowType WindowOrId = Union[WindowType, int]