Work on porting kittens to use new key infrastructure

Also move type definitions into their own module
This commit is contained in:
Kovid Goyal 2021-01-14 21:35:48 +05:30
parent 0714fd376b
commit 027c5a57f1
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
30 changed files with 524 additions and 721 deletions

View File

@ -629,7 +629,8 @@ specification.
* No way to disambiguate :kbd:`Esc` keypresses, other than using 8-bit controls * No way to disambiguate :kbd:`Esc` keypresses, other than using 8-bit controls
which are undesirable for other reasons which are undesirable for other reasons
* Incorrectly claims special keys are sometimes encoded using ``CSI letter`` encodings when it * 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`` * :kbd:`ctrl+shift+tab`` should be ``CSI 9 ; 6 u`` not ``CSI 1 ; 5 Z``
(shift+tab is not a separate key from tab) (shift+tab is not a separate key from tab)
* No support for the :kbd:`super` modifier. * No support for the :kbd:`super` modifier.

View File

@ -9,7 +9,7 @@
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import BroadcastCLIOptions 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.rc.base import MATCH_TAB_OPTION, MATCH_WINDOW_OPTION
from kitty.remote_control import create_basic_command, encode_send from kitty.remote_control import create_basic_command, encode_send
from kitty.typing import KeyEventType, ScreenSize from kitty.typing import KeyEventType, ScreenSize
@ -59,15 +59,15 @@ def on_eot(self) -> None:
def on_key(self, key_event: KeyEventType) -> None: def on_key(self, key_event: KeyEventType) -> None:
if self.line_edit.on_key(key_event): if self.line_edit.on_key(key_event):
self.commit_line() self.commit_line()
if key_event.type is not RELEASE and not key_event.mods: if key_event.matches('enter'):
if key_event.key is K['ENTER']: self.write_broadcast_text('\r')
self.write_broadcast_text('\r') self.print('')
self.print('') self.line_edit.clear()
self.line_edit.clear() self.write(SAVE_CURSOR)
self.write(SAVE_CURSOR) return
return
ek = encode_key_event(key_event) ek = encode_key_event(key_event)
ek = standard_b64encode(ek.encode('utf-8')).decode('ascii')
self.write_broadcast_data('kitty-key:' + ek) self.write_broadcast_data('kitty-key:' + ek)
def write_broadcast_text(self, text: str) -> None: def write_broadcast_text(self, text: str) -> None:

View File

@ -22,7 +22,7 @@
from kitty.conf.utils import KittensKeyAction from kitty.conf.utils import KittensKeyAction
from kitty.constants import appname from kitty.constants import appname
from kitty.fast_data_types import wcswidth 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.options_stub import DiffOptions
from kitty.utils import ScreenSize 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) INITIALIZING, COLLECTED, DIFFED, COMMAND, MESSAGE = range(5)
ESCAPE = K['ESCAPE']
def generate_diff(collection: Collection, context: int) -> Union[str, Dict[str, Patch]]: 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.message = sanitize(_('No matches found'))
self.cmd.bell() self.cmd.bell()
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: def on_key_event(self, key_event: KeyEvent, in_bracketed_paste: bool = False) -> None:
if self.state is COMMAND: if key_event.text:
self.line_edit.on_text(text, in_bracketed_paste) if self.state is COMMAND:
self.draw_status_line() self.line_edit.on_text(key_event.text, in_bracketed_paste)
return self.draw_status_line()
if self.state is MESSAGE: return
self.state = DIFFED if self.state is MESSAGE:
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:
self.state = DIFFED self.state = DIFFED
self.draw_status_line() self.draw_status_line()
return return
if self.state is COMMAND: else:
if self.line_edit.on_key(key_event): if self.state is MESSAGE:
if not self.line_edit.current_input: if key_event.type is not EventType.RELEASE:
self.state = DIFFED self.state = DIFFED
self.draw_status_line() self.draw_status_line()
return return
if key_event.type is RELEASE: if self.state is COMMAND:
return if self.line_edit.on_key(key_event):
if self.state is COMMAND: if not self.line_edit.current_input:
if key_event.key is ESCAPE: self.state = DIFFED
self.state = DIFFED self.draw_status_line()
self.draw_status_line() return
if key_event.type is EventType.RELEASE:
return 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) action = self.shortcut_action(key_event)
if action is not None: if action is not None:
return self.perform_action(action) return self.perform_action(action)

View File

@ -17,9 +17,7 @@
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import HintsCLIOptions from kitty.cli_stub import HintsCLIOptions
from kitty.fast_data_types import set_clipboard_string from kitty.fast_data_types import set_clipboard_string
from kitty.key_encoding import ( from kitty.key_encoding import KeyEvent
KeyEvent, backspace_key, enter_key, key_defs as K
)
from kitty.typing import BossType, KittyCommonOpts from kitty.typing import BossType, KittyCommonOpts
from kitty.utils import ScreenSize, screen_size_function, set_primary_selection 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_HINT_ALPHABET = string.digits + string.ascii_lowercase
DEFAULT_REGEX = r'(?m)^\s*(.+)\s*$' DEFAULT_REGEX = r'(?m)^\s*(.+)\s*$'
ESCAPE = K['ESCAPE']
class Mark: class Mark:
@ -177,11 +174,11 @@ def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
self.draw_screen() self.draw_screen()
def on_key(self, key_event: KeyEvent) -> None: 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_input = self.current_input[:-1]
self.current_text = None self.current_text = None
self.draw_screen() self.draw_screen()
elif key_event is enter_key and self.current_input: elif key_event.matches('enter') and self.current_input:
try: try:
idx = decode_hint(self.current_input, self.alphabet) idx = decode_hint(self.current_input, self.alphabet)
self.chosen.append(self.index_map[idx]) self.chosen.append(self.index_map[idx])
@ -196,7 +193,7 @@ def on_key(self, key_event: KeyEvent) -> None:
self.draw_screen() self.draw_screen()
else: else:
self.quit_loop(0) self.quit_loop(0)
elif key_event.key is ESCAPE: elif key_event.matches('esc'):
self.quit_loop(0 if self.multiple else 1) self.quit_loop(0 if self.multiple else 1)
def on_interrupt(self) -> None: def on_interrupt(self) -> None:
@ -288,6 +285,7 @@ def quotes(text: str, s: int, e: int) -> Tuple[int, int]:
@postprocessor @postprocessor
def ip(text: str, s: int, e: int) -> Tuple[int, int]: def ip(text: str, s: int, e: int) -> Tuple[int, int]:
from ipaddress import ip_address from ipaddress import ip_address
# Check validity of IPs (or raise InvalidMatch) # Check validity of IPs (or raise InvalidMatch)
ip = text[s:e] ip = text[s:e]

View File

@ -8,6 +8,7 @@
Any, Callable, ContextManager, Dict, Optional, Sequence, Type, Union Any, Callable, ContextManager, Dict, Optional, Sequence, Type, Union
) )
from kitty.types import ParsedShortcut
from kitty.typing import ( from kitty.typing import (
AbstractEventLoop, BossType, Debug, ImageManagerType, KeyEventType, AbstractEventLoop, BossType, Debug, ImageManagerType, KeyEventType,
KittensKeyActionType, LoopType, MouseEvent, ScreenSize, TermManagerType KittensKeyActionType, LoopType, MouseEvent, ScreenSize, TermManagerType
@ -35,6 +36,7 @@ def _initialize(
self.debug = debug self.debug = debug
self.cmd = commander(self) self.cmd = commander(self)
self._image_manager = image_manager self._image_manager = image_manager
self._key_shortcuts: Dict[ParsedShortcut, KittensKeyActionType] = {}
@property @property
def image_manager(self) -> ImageManagerType: def image_manager(self) -> ImageManagerType:
@ -45,18 +47,16 @@ def image_manager(self) -> ImageManagerType:
def asyncio_loop(self) -> AbstractEventLoop: def asyncio_loop(self) -> AbstractEventLoop:
return self._tui_loop.asycio_loop return self._tui_loop.asycio_loop
def add_shortcut(self, action: KittensKeyActionType, key: str, mods: Optional[int] = None, is_text: Optional[bool] = False) -> None: def add_shortcut(self, action: KittensKeyActionType, spec: Union[str, ParsedShortcut]) -> None:
if not hasattr(self, '_text_shortcuts'): if isinstance(spec, str):
self._text_shortcuts, self._key_shortcuts = {}, {} from kitty.key_encoding import parse_shortcut
if is_text: spec = parse_shortcut(spec)
self._text_shortcuts[key] = action self._key_shortcuts[spec] = action
else:
self._key_shortcuts[(key, mods or 0)] = action
def shortcut_action(self, key_event_or_text: Union[str, KeyEventType]) -> Optional[KittensKeyActionType]: def shortcut_action(self, key_event: KeyEventType) -> Optional[KittensKeyActionType]:
if isinstance(key_event_or_text, str): for sc, action in self._key_shortcuts.items():
return self._text_shortcuts.get(key_event_or_text) if key_event.matches(sc):
return self._key_shortcuts.get((key_event_or_text.key, key_event_or_text.mods)) return action
def __enter__(self) -> None: def __enter__(self) -> None:
if self._image_manager is not 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: def on_term(self) -> None:
self._tui_loop.quit(1) 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: def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
pass pass

View File

@ -5,17 +5,10 @@
from typing import Callable, Tuple from typing import Callable, Tuple
from kitty.fast_data_types import truncate_point_for_length, wcswidth 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 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: class LineEdit:
@ -137,22 +130,22 @@ def end(self) -> bool:
return self.cursor_pos != orig return self.cursor_pos != orig
def on_key(self, key_event: KeyEvent) -> bool: def on_key(self, key_event: KeyEvent) -> bool:
if key_event.type is RELEASE: if key_event.type is EventType.RELEASE:
return False return False
elif key_event.key is HOME: if key_event.matches('home'):
return self.home() return self.home()
elif key_event.key is END: if key_event.matches('end'):
return self.end() return self.end()
elif key_event.key is BACKSPACE: if key_event.matches('backspace'):
self.backspace() self.backspace()
return True return True
elif key_event.key is DELETE: if key_event.matches('delete'):
self.delete() self.delete()
return True return True
elif key_event.key is LEFT: if key_event.matches('left'):
self.left() self.left()
return True return True
elif key_event.key is RIGHT: if key_event.matches('right'):
self.right() self.right()
return True return True
return False return False

View File

@ -21,7 +21,7 @@
) )
from kitty.key_encoding import ( from kitty.key_encoding import (
ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, backspace_key, decode_key_event, 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.typing import ImageManagerType, KeyEventType, Protocol
from kitty.utils import ScreenSizeGetter, screen_size_function, write_all from kitty.utils import ScreenSizeGetter, screen_size_function, write_all
@ -29,8 +29,6 @@
from .handler import Handler from .handler import Handler
from .operations import init_state, reset_state from .operations import init_state, reset_state
C, D = K['C'], K['D']
class BinaryWrite(Protocol): class BinaryWrite(Protocol):
@ -149,7 +147,7 @@ def initialize(self) -> None:
self.write('Press the Enter key to quit') self.write('Press the Enter key to quit')
def on_key(self, key_event: KeyEventType) -> None: def on_key(self, key_event: KeyEventType) -> None:
if key_event is enter_key: if key_event.key == 'ENTER':
self.quit_loop(1) self.quit_loop(1)
def on_interrupt(self) -> None: def on_interrupt(self) -> None:
@ -282,8 +280,23 @@ def _on_csi(self, csi: str) -> None:
elif q == '~': elif q == '~':
if csi == '200~': if csi == '200~':
self.in_bracketed_paste = True self.in_bracketed_paste = True
return
elif csi == '201~': elif csi == '201~':
self.in_bracketed_paste = False 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: def _on_pm(self, pm: str) -> None:
pass 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) self.handler.on_clipboard_response(standard_b64decode(rest).decode('utf-8'), from_primary)
def _on_apc(self, apc: str) -> None: def _on_apc(self, apc: str) -> None:
if apc.startswith('K'): if apc.startswith('G'):
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 self.handler.image_manager is not None: if self.handler.image_manager is not None:
self.handler.image_manager.handle_response(apc) self.handler.image_manager.handle_response(apc)
# }}} # }}}

View File

@ -40,7 +40,6 @@
MOUSE_URXVT_MODE=(1015, '?'), MOUSE_URXVT_MODE=(1015, '?'),
ALTERNATE_SCREEN=(1049, '?'), ALTERNATE_SCREEN=(1049, '?'),
BRACKETED_PASTE=(2004, '?'), BRACKETED_PASTE=(2004, '?'),
EXTENDED_KEYBOARD=(2017, '?'),
) )
F = TypeVar('F') 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('MOUSE_MOTION_TRACKING') + reset_mode('MOUSE_MOVE_TRACKING') +
reset_mode('FOCUS_TRACKING') + reset_mode('MOUSE_UTF8_MODE') + reset_mode('FOCUS_TRACKING') + reset_mode('MOUSE_UTF8_MODE') +
reset_mode('MOUSE_SGR_MODE') + reset_mode('MOUSE_UTF8_MODE') + reset_mode('MOUSE_SGR_MODE') + reset_mode('MOUSE_UTF8_MODE') +
set_mode('BRACKETED_PASTE') + set_mode('EXTENDED_KEYBOARD') + set_mode('BRACKETED_PASTE') + SAVE_COLORS +
SAVE_COLORS + '\033[>31u' + # extended keyboard mode
'\033[*x' # reset DECSACE to default region select '\033[*x' # reset DECSACE to default region select
) )
if alternate_screen: if alternate_screen:
@ -287,6 +286,7 @@ def reset_state(normal_screen: bool = True) -> str:
ans += RESTORE_PRIVATE_MODE_VALUES ans += RESTORE_PRIVATE_MODE_VALUES
ans += RESTORE_CURSOR ans += RESTORE_CURSOR
ans += RESTORE_COLORS ans += RESTORE_COLORS
ans += '\033[<u' # restore keyboard mode
return ans return ans

View File

@ -25,8 +25,7 @@
) )
from .config_data import MINIMUM_FONT_SIZE from .config_data import MINIMUM_FONT_SIZE
from .constants import ( from .constants import (
SingleKey, appname, config_dir, is_macos, kitty_exe, appname, config_dir, is_macos, kitty_exe, supports_primary_selection
supports_primary_selection
) )
from .fast_data_types import ( from .fast_data_types import (
CLOSE_BEING_CONFIRMED, IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED, CLOSE_BEING_CONFIRMED, IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED,
@ -49,6 +48,7 @@
from .tabs import ( from .tabs import (
SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager
) )
from .types import SingleKey
from .typing import PopenType, TypedDict from .typing import PopenType, TypedDict
from .utils import ( from .utils import (
func_name, get_editor, get_primary_selection, is_path_in_temp_dir, func_name, get_editor, get_primary_selection, is_path_in_temp_dir,

View File

@ -14,8 +14,9 @@
from .cli_stub import CLIOptions from .cli_stub import CLIOptions
from .conf.utils import resolve_config from .conf.utils import resolve_config
from .config import KeyAction from .config import KeyAction
from .constants import appname, defconf, is_macos, is_wayland, str_version, SingleKey from .constants import appname, defconf, is_macos, is_wayland, str_version
from .options_stub import Options as OptionsStub from .options_stub import Options as OptionsStub
from .types import SingleKey
from .typing import BadLineType, SequenceMap, TypedDict from .typing import BadLineType, SequenceMap, TypedDict
@ -759,7 +760,10 @@ def parse_args(
def print_shortcut(key_sequence: Iterable[SingleKey], action: KeyAction) -> None: def print_shortcut(key_sequence: Iterable[SingleKey], action: KeyAction) -> 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} mmap = {'shift': GLFW_MOD_SHIFT, 'alt': GLFW_MOD_ALT, 'ctrl': GLFW_MOD_CONTROL, ('cmd' if is_macos else 'super'): GLFW_MOD_SUPER}
keys = [] keys = []
for key_spec in key_sequence: for key_spec in key_sequence:

View File

@ -11,7 +11,8 @@
) )
from ..rgb import Color, to_color as as_color 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+(.+)$') key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
T = TypeVar('T') T = TypeVar('T')
@ -301,38 +302,6 @@ def w(f: Callable) -> Callable:
return func_with_args, ans 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, ...]] 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) return func, tuple(args)
KittensKey = Tuple[str, Optional[int], bool]
def parse_kittens_key( def parse_kittens_key(
val: str, funcs_with_args: Dict[str, Callable] 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] sc, action = val.partition(' ')[::2]
if not sc or not action: if not sc or not action:
return None return None
mods, key, is_text = parse_kittens_shortcut(sc)
ans = parse_kittens_func_args(action, funcs_with_args) ans = parse_kittens_func_args(action, funcs_with_args)
return ans, (key, mods, is_text) return ans, parse_shortcut(sc)

View File

@ -20,9 +20,10 @@
parse_config_base, python_string, to_bool, to_cmdline parse_config_base, python_string, to_bool, to_cmdline
) )
from .config_data import InvalidMods, all_options, parse_shortcut, type_convert 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 .fonts import FontFeature
from .options_stub import Options as OptionsStub from .options_stub import Options as OptionsStub
from .types import SingleKey
from .typing import TypedDict from .typing import TypedDict
from .utils import expandvars, log_error from .utils import expandvars, log_error

View File

@ -16,13 +16,15 @@
choices, positive_float, positive_int, to_bool, to_cmdline as tc, to_color, choices, positive_float, positive_int, to_bool, to_cmdline as tc, to_color,
to_color_or_none, unit_float to_color_or_none, unit_float
) )
from .constants import ( from .constants import config_dir, is_macos
FloatEdges, SingleKey, config_dir, is_macos
)
from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE 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 .layout.interface import all_layouts
from .rgb import Color, color_as_int, color_as_sharp, color_from_int from .rgb import Color, color_as_int, color_as_sharp, color_from_int
from .types import FloatEdges, SingleKey
from .utils import log_error from .utils import log_error
@ -60,6 +62,8 @@ def to_modifiers(val: str) -> int:
def parse_shortcut(sc: str) -> SingleKey: def parse_shortcut(sc: str) -> SingleKey:
if sc.endswith('+') and len(sc) > 1:
sc = sc[:-1] + 'plus'
parts = sc.split('+') parts = sc.split('+')
mods = 0 mods = 0
if len(parts) > 1: if len(parts) > 1:
@ -67,6 +71,7 @@ def parse_shortcut(sc: str) -> SingleKey:
if not mods: if not mods:
raise InvalidMods('Invalid shortcut') raise InvalidMods('Invalid shortcut')
q = parts[-1] q = parts[-1]
q = character_key_name_aliases.get(q.upper(), q)
is_native = False is_native = False
if q.startswith('0x'): if q.startswith('0x'):
try: try:
@ -80,7 +85,7 @@ def parse_shortcut(sc: str) -> SingleKey:
key = ord(q) key = ord(q)
except Exception: except Exception:
uq = q.upper() 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) x: Optional[int] = getattr(defines, f'GLFW_FKEY_{uq}', None)
if x is None: if x is None:
lf = get_key_name_lookup() lf = get_key_name_lookup()

View File

@ -27,45 +27,6 @@ class Version(NamedTuple):
base = os.path.dirname(os.path.abspath(__file__)) 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) @lru_cache(maxsize=2)
def kitty_exe() -> str: def kitty_exe() -> str:
rpath = sys._xoptions.get('bundle_exe_dir') rpath = sys._xoptions.get('bundle_exe_dir')

822
kitty/key_encoding.py generated
View File

@ -2,476 +2,254 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import string from enum import IntEnum
from typing import Dict, NamedTuple, Optional from functools import lru_cache
from typing import Dict, NamedTuple, Optional, Tuple, Union
from . import fast_data_types as defines 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 {{{ # number name mappings {{{
ENCODING = { # start csi mapping (auto generated by gen-key-constants.py do not edit)
'0': 'G', functional_key_number_to_name_map = {
'1': 'H', 57344: 'ESCAPE',
'2': 'I', 57345: 'ENTER',
'3': 'J', 57346: 'TAB',
'4': 'K', 57347: 'BACKSPACE',
'5': 'L', 57348: 'INSERT',
'6': 'M', 57349: 'DELETE',
'7': 'N', 57350: 'LEFT',
'8': 'O', 57351: 'RIGHT',
'9': 'P', 57352: 'UP',
'A': 'S', 57353: 'DOWN',
'APOSTROPHE': 'B', 57354: 'PAGE_UP',
'B': 'T', 57355: 'PAGE_DOWN',
'BACKSLASH': 't', 57356: 'HOME',
'BACKSPACE': '1', 57357: 'END',
'C': 'U', 57358: 'CAPS_LOCK',
'CAPS LOCK': ':', 57359: 'SCROLL_LOCK',
'COMMA': 'C', 57360: 'NUM_LOCK',
'D': 'V', 57361: 'PRINT_SCREEN',
'DELETE': '3', 57362: 'PAUSE',
'DOWN': '6', 57363: 'MENU',
'E': 'W', 57364: 'F1',
'END': '-', 57365: 'F2',
'ENTER': 'z', 57366: 'F3',
'EQUAL': 'R', 57367: 'F4',
'ESCAPE': 'y', 57368: 'F5',
'F': 'X', 57369: 'F6',
'F1': '/', 57370: 'F7',
'F10': ']', 57371: 'F8',
'F11': '{', 57372: 'F9',
'F12': '}', 57373: 'F10',
'F13': '@', 57374: 'F11',
'F14': '%', 57375: 'F12',
'F15': '$', 57376: 'F13',
'F16': '#', 57377: 'F14',
'F17': 'BA', 57378: 'F15',
'F18': 'BB', 57379: 'F16',
'F19': 'BC', 57380: 'F17',
'F2': '*', 57381: 'F18',
'F20': 'BD', 57382: 'F19',
'F21': 'BE', 57383: 'F20',
'F22': 'BF', 57384: 'F21',
'F23': 'BG', 57385: 'F22',
'F24': 'BH', 57386: 'F23',
'F25': 'BI', 57387: 'F24',
'F3': '?', 57388: 'F25',
'F4': '&', 57389: 'F26',
'F5': '<', 57390: 'F27',
'F6': '>', 57391: 'F28',
'F7': '(', 57392: 'F29',
'F8': ')', 57393: 'F30',
'F9': '[', 57394: 'F31',
'G': 'Y', 57395: 'F32',
'GRAVE ACCENT': 'v', 57396: 'F33',
'H': 'Z', 57397: 'F34',
'HOME': '.', 57398: 'F35',
'I': 'a', 57399: 'KP_0',
'INSERT': '2', 57400: 'KP_1',
'J': 'b', 57401: 'KP_2',
'K': 'c', 57402: 'KP_3',
'KP 0': 'BJ', 57403: 'KP_4',
'KP 1': 'BK', 57404: 'KP_5',
'KP 2': 'BL', 57405: 'KP_6',
'KP 3': 'BM', 57406: 'KP_7',
'KP 4': 'BN', 57407: 'KP_8',
'KP 5': 'BO', 57408: 'KP_9',
'KP 6': 'BP', 57409: 'KP_DECIMAL',
'KP 7': 'BQ', 57410: 'KP_DIVIDE',
'KP 8': 'BR', 57411: 'KP_MULTIPLY',
'KP 9': 'BS', 57412: 'KP_SUBTRACT',
'KP ADD': 'BX', 57413: 'KP_ADD',
'KP DECIMAL': 'BT', 57414: 'KP_ENTER',
'KP DIVIDE': 'BU', 57415: 'KP_EQUAL',
'KP ENTER': 'BY', 57416: 'KP_SEPARATOR',
'KP EQUAL': 'BZ', 57417: 'KP_LEFT',
'KP MULTIPLY': 'BV', 57418: 'KP_RIGHT',
'KP SUBTRACT': 'BW', 57419: 'KP_UP',
'L': 'd', 57420: 'KP_DOWN',
'LEFT': '5', 57421: 'KP_PAGE_UP',
'LEFT ALT': 'Bc', 57422: 'KP_PAGE_DOWN',
'LEFT BRACKET': 's', 57423: 'KP_HOME',
'LEFT CONTROL': 'Bb', 57424: 'KP_END',
'LEFT SHIFT': 'Ba', 57425: 'KP_INSERT',
'LEFT SUPER': 'Bd', 57426: 'KP_DELETE',
'M': 'e', 57427: 'LEFT_SHIFT',
'MINUS': 'D', 57428: 'LEFT_CONTROL',
'N': 'f', 57429: 'LEFT_ALT',
'NUM LOCK': '=', 57430: 'LEFT_SUPER',
'O': 'g', 57431: 'RIGHT_SHIFT',
'P': 'h', 57432: 'RIGHT_CONTROL',
'PAGE DOWN': '9', 57433: 'RIGHT_ALT',
'PAGE UP': '8', 57434: 'RIGHT_SUPER',
'PAUSE': '!', 57435: 'MEDIA_PLAY',
'PERIOD': 'E', 57436: 'MEDIA_PAUSE',
'PRINT SCREEN': '^', 57437: 'MEDIA_PLAY_PAUSE',
'Q': 'i', 57438: 'MEDIA_REVERSE',
'R': 'j', 57439: 'MEDIA_STOP',
'RIGHT': '4', 57440: 'MEDIA_FAST_FORWARD',
'RIGHT ALT': 'Bg', 57441: 'MEDIA_REWIND',
'RIGHT BRACKET': 'u', 57442: 'MEDIA_TRACK_NEXT',
'RIGHT CONTROL': 'Bf', 57443: 'MEDIA_TRACK_PREVIOUS',
'RIGHT SHIFT': 'Be', 57444: 'MEDIA_RECORD',
'RIGHT SUPER': 'Bh', 57445: 'LOWER_VOLUME',
'S': 'k', 57446: 'RAISE_VOLUME',
'SCROLL LOCK': '+', 57447: 'MUTE_VOLUME'}
'SEMICOLON': 'Q', csi_number_to_functional_number_map = {
'SLASH': 'F', 2: 57348,
'SPACE': 'A', 3: 57349,
'T': 'l', 5: 57354,
'TAB': '0', 6: 57355,
'U': 'm', 7: 57356,
'UP': '7', 8: 57357,
'V': 'n', 9: 57346,
'W': 'o', 11: 57364,
'WORLD 1': 'w', 12: 57365,
'WORLD 2': 'x', 13: 57345,
'X': 'p', 14: 57367,
'Y': 'q', 15: 57368,
'Z': 'r', 17: 57369,
'PLUS': 'Bi', 18: 57370,
'UNDERSCORE': 'Bj', 19: 57371,
'MENU': 'Bk', 20: 57372,
'EXCLAM': 'Bl', 21: 57373,
'DOUBLE QUOTE': 'Bm', 23: 57374,
'NUMBER SIGN': 'Bn', 24: 57375,
'DOLLAR': 'Bo', 27: 57344,
'AMPERSAND': 'Bp', 127: 57347}
'PARENTHESIS LEFT': 'Bq', 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}
'PARENTHESIS RIGHT': 'Br', tilde_trailers = {57348, 57349, 57354, 57355, 57368, 57369, 57370, 57371, 57372, 57373, 57374, 57375}
'COLON': 'Bs', # end csi mapping
'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 +
'`~!@#$%^&*()_-+=[{]}\\|<,>./?;:\'" '
'ÄäÖöÜüߧºàåæçèéìñòøùабвгдежзийклмнопрстуфхцчшщъыьэюяё'
)
def text_match(key: str) -> Optional[str]: @lru_cache(2)
if key.upper() == 'SPACE': def get_name_to_functional_number_map() -> Dict[str, int]:
return ' ' return {v: k for k, v in functional_key_number_to_name_map.items()}
if key not in text_keys:
return None
return key
def encode( @lru_cache(2)
integer: int, def get_functional_to_csi_number_map() -> Dict[int, int]:
chars: str = string.ascii_uppercase + string.ascii_lowercase + string.digits + return {v: k for k, v in csi_number_to_functional_number_map.items()}
'.-:+=^!/*?&<>()[]{}@%$#'
) -> str:
ans = ''
d = len(chars)
while True:
integer, remainder = divmod(integer, d)
ans = chars[remainder] + ans
if integer == 0:
break
return ans
def symbolic_name(glfw_name: str) -> str: @lru_cache(2)
return glfw_name[9:].replace('_', ' ') 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()}
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
PRESS: int = 1 PRESS: int = 1
REPEAT: int = 2 REPEAT: int = 2
RELEASE: int = 4 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 SHIFT, ALT, CTRL, SUPER = 1, 2, 4, 8
type_map = {'p': PRESS, 't': REPEAT, 'r': RELEASE} enter_key = KeyEvent(key='ENTER')
rtype_map = {v: k for k, v in type_map.items()} backspace_key = KeyEvent(key='BACKSPACE')
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 = {}
config_mod_map = { config_mod_map = {
'SHIFT': SHIFT, 'SHIFT': SHIFT,
'ALT': ALT, 'ALT': ALT,
@ -483,56 +261,102 @@ class KeyEvent(NamedTuple):
'CTRL': CTRL, 'CTRL': CTRL,
'CONTROL': 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: def decode_key_event(csi: str, csi_type: str) -> KeyEvent:
typ = type_map[text[0]] parts = csi.split(';')
mods = mod_map[text[1]]
key = key_rmap[text[2:4]] def get_sub_sections(x: str, missing: int = 0) -> Tuple[int, ...]:
return KeyEvent(typ, mods, key) 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: def encode_key_event(key_event: KeyEvent) -> str:
typ = rtype_map[key_event.type] key = csi_number_for_name(key_event.key)
mods = rmod_map[key_event.mods] shifted_key = csi_number_for_name(key_event.shifted_key)
key = ENCODING[key_event.key.replace('_', ' ')] alternate_key = csi_number_for_name(key_event.alternate_key)
return typ + mods + key lt = get_csi_number_to_letter_trailer_map()
trailer = lt.get(key, 'u')
if trailer != 'u':
class WindowSystemKeyEvent(NamedTuple): key = 1
code: int mods = key_event.mods
mods: int text = key_event.text
action: int 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]: def decode_key_event_as_window_system_key(text: str) -> Optional[WindowSystemKeyEvent]:
k = decode_key_event(text) csi, trailer = text[2:-1], text[-1]
glfw_name = glfw_key_name(k.key) try:
glfw_code = getattr(defines, glfw_name, None) k = decode_key_event(csi, trailer)
if glfw_code is None: except Exception:
return None return None
action = defines.GLFW_PRESS return k.as_window_system_event()
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)

View File

@ -9,9 +9,7 @@
from .constants import is_macos from .constants import is_macos
key_name_aliases = { functional_key_name_aliases = {
'SPACE': ' ',
'SPC': ' ',
'ESC': 'ESCAPE', 'ESC': 'ESCAPE',
'PGUP': 'PAGE_UP', 'PGUP': 'PAGE_UP',
'PAGEUP': 'PAGE_UP', 'PAGEUP': 'PAGE_UP',
@ -21,7 +19,17 @@
'ARROWUP': 'UP', 'ARROWUP': 'UP',
'ARROWDOWN': 'DOWN', 'ARROWDOWN': 'DOWN',
'ARROWRIGHT': 'RIGHT', 'ARROWRIGHT': 'RIGHT',
'ARROWLEFT': 'LEFT' 'ARROWLEFT': 'LEFT',
'DEL': 'DELETE',
}
character_key_name_aliases = {
'SPC': ' ',
'SPACE': ' ',
'PLUS': '+',
'MINUS': '-',
'HYPHEN': '-',
} }
LookupFunc = Callable[[str, bool], Optional[int]] LookupFunc = Callable[[str, bool], Optional[int]]

View File

@ -5,8 +5,8 @@
from typing import Optional, Union from typing import Optional, Union
from .config import KeyAction, KeyMap, SequenceMap, SubSequenceMap from .config import KeyAction, KeyMap, SequenceMap, SubSequenceMap
from .constants import SingleKey
from .fast_data_types import KeyEvent from .fast_data_types import KeyEvent
from .types import SingleKey
from .typing import ScreenType from .typing import ScreenType

View File

@ -9,14 +9,14 @@
Tuple Tuple
) )
from kitty.constants import Edges, WindowGeometry from kitty.borders import BorderColor
from kitty.fast_data_types import ( from kitty.fast_data_types import (
Region, set_active_window, viewport_for_window Region, set_active_window, viewport_for_window
) )
from kitty.options_stub import Options from kitty.options_stub import Options
from kitty.types import Edges, WindowGeometry
from kitty.typing import TypedDict, WindowType from kitty.typing import TypedDict, WindowType
from kitty.window_list import WindowGroup, WindowList from kitty.window_list import WindowGroup, WindowList
from kitty.borders import BorderColor
class BorderLine(NamedTuple): class BorderLine(NamedTuple):

View File

@ -10,7 +10,7 @@
) )
from kitty.borders import BorderColor from kitty.borders import BorderColor
from kitty.constants import Edges from kitty.types import Edges
from kitty.typing import WindowType from kitty.typing import WindowType
from kitty.window_list import WindowGroup, WindowList from kitty.window_list import WindowGroup, WindowList

View File

@ -8,7 +8,7 @@
) )
from kitty.borders import BorderColor 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.typing import EdgeLiteral, WindowType
from kitty.window_list import WindowGroup, WindowList from kitty.window_list import WindowGroup, WindowList

View File

@ -7,7 +7,7 @@
from kitty.borders import BorderColor from kitty.borders import BorderColor
from kitty.conf.utils import to_bool 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.typing import EdgeLiteral, WindowType
from kitty.window_list import WindowGroup, WindowList from kitty.window_list import WindowGroup, WindowList

View File

@ -5,7 +5,7 @@
from typing import Dict, Generator, Iterable, List, Tuple from typing import Dict, Generator, Iterable, List, Tuple
from kitty.borders import BorderColor from kitty.borders import BorderColor
from kitty.constants import Edges from kitty.types import Edges
from kitty.typing import WindowType from kitty.typing import WindowType
from kitty.window_list import WindowGroup, WindowList from kitty.window_list import WindowGroup, WindowList

View File

@ -17,7 +17,7 @@
from .conf.utils import BadLine from .conf.utils import BadLine
from .config import cached_values_for from .config import cached_values_for
from .constants import ( 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 is_wayland, kitty_exe, logo_data_file, running_in_kitty
) )
from .fast_data_types import ( from .fast_data_types import (
@ -30,6 +30,7 @@
from .options_stub import Options as OptionsStub from .options_stub import Options as OptionsStub
from .os_window_size import initial_window_size_func from .os_window_size import initial_window_size_func
from .session import get_os_window_sizing_data from .session import get_os_window_sizing_data
from .types import SingleKey
from .utils import ( from .utils import (
detach, expandvars, find_exe, log_error, read_shell_environment, detach, expandvars, find_exe, log_error, read_shell_environment,
single_instance, startup_notification_handler, unix_socket_paths single_instance, startup_notification_handler, unix_socket_paths

View File

@ -34,10 +34,11 @@ def generate_stub():
all_options, all_options,
class_name='DiffOptions', class_name='DiffOptions',
preamble_lines=( preamble_lines=(
'from kitty.conf.utils import KittensKey, KittensKeyAction', 'from kitty.conf.utils import KittensKeyAction',
'from kitty.types import ParsedShortcut',
), ),
extra_fields=( extra_fields=(
('key_definitions', 'typing.Dict[KittensKey, KittensKeyAction]'), ('key_definitions', 'typing.Dict[ParsedShortcut, KittensKeyAction]'),
) )
) )

View File

@ -4,7 +4,8 @@
from typing import Any, Callable, Dict, NamedTuple, Tuple 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 .typing import EdgeLiteral
from .utils import log_error from .utils import log_error

View File

@ -8,9 +8,9 @@
from typing import TYPE_CHECKING, Dict, Generator, List, Optional from typing import TYPE_CHECKING, Dict, Generator, List, Optional
from kitty.config import parse_send_text_bytes from kitty.config import parse_send_text_bytes
from kitty.key_encoding import ( from kitty.key_encoding import decode_key_event_as_window_system_key
WindowSystemKeyEvent, decode_key_event_as_window_system_key from kitty.fast_data_types import KeyEvent as WindowSystemKeyEvent
)
from .base import ( from .base import (
MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError,
PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType, PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType,
@ -145,7 +145,8 @@ def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get:
elif encoding == 'base64': elif encoding == 'base64':
data = base64.standard_b64decode(q) data = base64.standard_b64decode(q)
elif encoding == 'kitty-key': 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: else:
raise TypeError(f'Invalid encoding for send-text data: {encoding}') raise TypeError(f'Invalid encoding for send-text data: {encoding}')
exclude_active = payload_get('exclude_active') 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 window is not None:
if not exclude_active or window is not boss.active_window: if not exclude_active or window is not boss.active_window:
if isinstance(data, WindowSystemKeyEvent): if isinstance(data, WindowSystemKeyEvent):
kdata = window.encoded_key(data.code, mods=data.mods, action=data.action) kdata = window.encoded_key(data)
if kdata: if kdata:
window.write_to_child(kdata) window.write_to_child(kdata)
else: else:

View File

@ -6,7 +6,7 @@
from typing import Any, Dict, NamedTuple, Optional, Sequence, Tuple from typing import Any, Dict, NamedTuple, Optional, Sequence, Tuple
from .config import build_ansi_color_table from .config import build_ansi_color_table
from .constants import WindowGeometry from .types import WindowGeometry
from .fast_data_types import ( from .fast_data_types import (
DECAWM, Screen, cell_size_for_window, pt_to_px, set_tab_bar_render_data, DECAWM, Screen, cell_size_for_window, pt_to_px, set_tab_bar_render_data,
viewport_for_window viewport_for_window

49
kitty/types.py Normal file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
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

View File

@ -19,13 +19,13 @@
from .child import ProcessDesc from .child import ProcessDesc
from .cli_stub import CLIOptions from .cli_stub import CLIOptions
from .config import build_ansi_color_table from .config import build_ansi_color_table
from .constants import ScreenGeometry, WindowGeometry, appname, wakeup from .constants import appname, wakeup
from .fast_data_types import ( from .fast_data_types import (
BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM, BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM,
CELL_PROGRAM, CELL_SPECIAL_PROGRAM, DCS, DECORATION, DIM, GLFW_MOD_CONTROL, CELL_PROGRAM, CELL_SPECIAL_PROGRAM, DCS, DECORATION, DIM, GLFW_MOD_CONTROL,
GLFW_PRESS, GRAPHICS_ALPHA_MASK_PROGRAM, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_ALPHA_MASK_PROGRAM, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_PROGRAM,
GRAPHICS_PROGRAM, MARK, MARK_MASK, OSC, REVERSE, SCROLL_FULL, SCROLL_LINE, MARK, MARK_MASK, OSC, REVERSE, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE,
SCROLL_PAGE, STRIKETHROUGH, TINT_PROGRAM, Screen, add_timer, add_window, STRIKETHROUGH, TINT_PROGRAM, KeyEvent, Screen, add_timer, add_window,
cell_size_for_window, compile_program, encode_key_for_tty, get_boss, cell_size_for_window, compile_program, encode_key_for_tty, get_boss,
get_clipboard_string, init_cell_program, pt_to_px, set_clipboard_string, get_clipboard_string, init_cell_program, pt_to_px, set_clipboard_string,
set_titlebar_color, set_window_padding, set_window_render_data, set_titlebar_color, set_window_padding, set_window_render_data,
@ -36,6 +36,7 @@
from .options_stub import Options from .options_stub import Options
from .rgb import to_color from .rgb import to_color
from .terminfo import get_capabilities from .terminfo import get_capabilities
from .types import ScreenGeometry, WindowGeometry
from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict
from .utils import ( from .utils import (
color_as_int, get_primary_selection, load_shaders, open_cmd, open_url, color_as_int, get_primary_selection, load_shaders, open_cmd, open_url,
@ -881,18 +882,20 @@ def copy_to_clipboard(self) -> None:
if text: if text:
set_clipboard_string(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( return encode_key_for_tty(
key=key, mods=mods, key_encoding_flags=self.screen.current_key_encoding_flags(), key=key_event.key, shifted_key=key_event.shifted_key, alternate_key=key_event.alternate_key,
cursor_key_mode=self.screen.cursor_key_mode, action=action mods=key_event.mods, action=key_event.action, text=key_event.text,
).encode('ascii') 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: def copy_or_interrupt(self) -> None:
text = self.text_for_selection() text = self.text_for_selection()
if text: if text:
set_clipboard_string(text) set_clipboard_string(text)
else: 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: def copy_and_clear_or_interrupt(self) -> None:
self.copy_or_interrupt() self.copy_or_interrupt()

View File

@ -10,7 +10,7 @@
Any, Deque, Dict, Generator, Iterator, List, Optional, Tuple, Union Any, Deque, Dict, Generator, Iterator, List, Optional, Tuple, Union
) )
from .constants import WindowGeometry from .types import WindowGeometry
from .typing import EdgeLiteral, TabType, WindowType from .typing import EdgeLiteral, TabType, WindowType
WindowOrId = Union[WindowType, int] WindowOrId = Union[WindowType, int]