Implement key event parsing and matching

This commit is contained in:
Kovid Goyal 2022-08-24 08:30:21 +05:30
parent 63fdbd3fa0
commit d6ed20323b
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 227 additions and 3 deletions

View File

@ -3,16 +3,18 @@
import os import os
import subprocess import subprocess
from typing import Dict, List, Tuple from typing import Dict, List, Tuple, Union
import kitty.constants as kc import kitty.constants as kc
from kittens.tui.operations import Mode from kittens.tui.operations import Mode
from kitty.cli import OptionDict, OptionSpecSeq, parse_option_spec from kitty.cli import OptionDict, OptionSpecSeq, parse_option_spec
from kitty.rc.base import RemoteCommand, all_command_names, command_for_name from kitty.rc.base import RemoteCommand, all_command_names, command_for_name
from kitty.key_names import functional_key_name_aliases, character_key_name_aliases
from kitty.key_encoding import config_mod_map
def serialize_as_go_string(x: str) -> str: def serialize_as_go_string(x: str) -> str:
return x.replace('\n', '\\n').replace('"', '\\"') return x.replace('\\', '\\\\').replace('\n', '\\n').replace('"', '\\"')
def replace(template: str, **kw: str) -> str: def replace(template: str, **kw: str) -> str:
@ -183,6 +185,19 @@ def build_go_code(name: str, cmd: RemoteCommand, seq: OptionSpecSeq, template: s
return ans return ans
def serialize_go_dict(x: Union[Dict[str, int], Dict[int, str], Dict[int, int], Dict[str, str]]) -> str:
ans = []
def s(x: Union[int, str]) -> str:
if isinstance(x, int):
return str(x)
return f'"{serialize_as_go_string(x)}"'
for k, v in x.items():
ans.append(f'{s(k)}: {s(v)}')
return '{' + ', '.join(ans) + '}'
def main() -> None: def main() -> None:
with open('constants_generated.go', 'w') as f: with open('constants_generated.go', 'w') as f:
dp = ", ".join(map(lambda x: f'"{serialize_as_go_string(x)}"', kc.default_pager_for_help)) dp = ", ".join(map(lambda x: f'"{serialize_as_go_string(x)}"', kc.default_pager_for_help))
@ -202,6 +217,9 @@ const IsFrozenBuild bool = false
const HandleTermiosSignals = {Mode.HANDLE_TERMIOS_SIGNALS.value[0]} const HandleTermiosSignals = {Mode.HANDLE_TERMIOS_SIGNALS.value[0]}
var Version VersionType = VersionType{{Major: {kc.version.major}, Minor: {kc.version.minor}, Patch: {kc.version.patch},}} var Version VersionType = VersionType{{Major: {kc.version.major}, Minor: {kc.version.minor}, Patch: {kc.version.patch},}}
var DefaultPager []string = []string{{ {dp} }} var DefaultPager []string = []string{{ {dp} }}
var FunctionalKeyNameAliases = map[string]string{serialize_go_dict(functional_key_name_aliases)}
var CharacterKeyNameAliases = map[string]string{serialize_go_dict(character_key_name_aliases)}
var ConfigModMap = map[string]uint16{serialize_go_dict(config_mod_map)}
''') ''')
with open('tools/cmd/at/template.go') as f: with open('tools/cmd/at/template.go') as f:
template = f.read() template = f.read()

View File

@ -7,7 +7,7 @@ from typing import Callable, Dict, Optional
from .constants import is_macos from .constants import is_macos
functional_key_name_aliases = { functional_key_name_aliases: Dict[str, str] = {
'ESC': 'ESCAPE', 'ESC': 'ESCAPE',
'PGUP': 'PAGE_UP', 'PGUP': 'PAGE_UP',
'PAGEUP': 'PAGE_UP', 'PAGEUP': 'PAGE_UP',

View File

@ -1,5 +1,12 @@
package tui package tui
import (
"strconv"
"strings"
"kitty"
)
// key encoding mappings {{{ // key encoding mappings {{{
// start csi mapping (auto generated by gen-key-constants.py do not edit) // start csi mapping (auto generated by gen-key-constants.py do not edit)
var functional_key_number_to_name_map = map[int]string{57344: "ESCAPE", 57345: "ENTER", 57346: "TAB", 57347: "BACKSPACE", 57348: "INSERT", 57349: "DELETE", 57350: "LEFT", 57351: "RIGHT", 57352: "UP", 57353: "DOWN", 57354: "PAGE_UP", 57355: "PAGE_DOWN", 57356: "HOME", 57357: "END", 57358: "CAPS_LOCK", 57359: "SCROLL_LOCK", 57360: "NUM_LOCK", 57361: "PRINT_SCREEN", 57362: "PAUSE", 57363: "MENU", 57364: "F1", 57365: "F2", 57366: "F3", 57367: "F4", 57368: "F5", 57369: "F6", 57370: "F7", 57371: "F8", 57372: "F9", 57373: "F10", 57374: "F11", 57375: "F12", 57376: "F13", 57377: "F14", 57378: "F15", 57379: "F16", 57380: "F17", 57381: "F18", 57382: "F19", 57383: "F20", 57384: "F21", 57385: "F22", 57386: "F23", 57387: "F24", 57388: "F25", 57389: "F26", 57390: "F27", 57391: "F28", 57392: "F29", 57393: "F30", 57394: "F31", 57395: "F32", 57396: "F33", 57397: "F34", 57398: "F35", 57399: "KP_0", 57400: "KP_1", 57401: "KP_2", 57402: "KP_3", 57403: "KP_4", 57404: "KP_5", 57405: "KP_6", 57406: "KP_7", 57407: "KP_8", 57408: "KP_9", 57409: "KP_DECIMAL", 57410: "KP_DIVIDE", 57411: "KP_MULTIPLY", 57412: "KP_SUBTRACT", 57413: "KP_ADD", 57414: "KP_ENTER", 57415: "KP_EQUAL", 57416: "KP_SEPARATOR", 57417: "KP_LEFT", 57418: "KP_RIGHT", 57419: "KP_UP", 57420: "KP_DOWN", 57421: "KP_PAGE_UP", 57422: "KP_PAGE_DOWN", 57423: "KP_HOME", 57424: "KP_END", 57425: "KP_INSERT", 57426: "KP_DELETE", 57427: "KP_BEGIN", 57428: "MEDIA_PLAY", 57429: "MEDIA_PAUSE", 57430: "MEDIA_PLAY_PAUSE", 57431: "MEDIA_REVERSE", 57432: "MEDIA_STOP", 57433: "MEDIA_FAST_FORWARD", 57434: "MEDIA_REWIND", 57435: "MEDIA_TRACK_NEXT", 57436: "MEDIA_TRACK_PREVIOUS", 57437: "MEDIA_RECORD", 57438: "LOWER_VOLUME", 57439: "RAISE_VOLUME", 57440: "MUTE_VOLUME", 57441: "LEFT_SHIFT", 57442: "LEFT_CONTROL", 57443: "LEFT_ALT", 57444: "LEFT_SUPER", 57445: "LEFT_HYPER", 57446: "LEFT_META", 57447: "RIGHT_SHIFT", 57448: "RIGHT_CONTROL", 57449: "RIGHT_ALT", 57450: "RIGHT_SUPER", 57451: "RIGHT_HYPER", 57452: "RIGHT_META", 57453: "ISO_LEVEL3_SHIFT", 57454: "ISO_LEVEL5_SHIFT"} var functional_key_number_to_name_map = map[int]string{57344: "ESCAPE", 57345: "ENTER", 57346: "TAB", 57347: "BACKSPACE", 57348: "INSERT", 57349: "DELETE", 57350: "LEFT", 57351: "RIGHT", 57352: "UP", 57353: "DOWN", 57354: "PAGE_UP", 57355: "PAGE_DOWN", 57356: "HOME", 57357: "END", 57358: "CAPS_LOCK", 57359: "SCROLL_LOCK", 57360: "NUM_LOCK", 57361: "PRINT_SCREEN", 57362: "PAUSE", 57363: "MENU", 57364: "F1", 57365: "F2", 57366: "F3", 57367: "F4", 57368: "F5", 57369: "F6", 57370: "F7", 57371: "F8", 57372: "F9", 57373: "F10", 57374: "F11", 57375: "F12", 57376: "F13", 57377: "F14", 57378: "F15", 57379: "F16", 57380: "F17", 57381: "F18", 57382: "F19", 57383: "F20", 57384: "F21", 57385: "F22", 57386: "F23", 57387: "F24", 57388: "F25", 57389: "F26", 57390: "F27", 57391: "F28", 57392: "F29", 57393: "F30", 57394: "F31", 57395: "F32", 57396: "F33", 57397: "F34", 57398: "F35", 57399: "KP_0", 57400: "KP_1", 57401: "KP_2", 57402: "KP_3", 57403: "KP_4", 57404: "KP_5", 57405: "KP_6", 57406: "KP_7", 57407: "KP_8", 57408: "KP_9", 57409: "KP_DECIMAL", 57410: "KP_DIVIDE", 57411: "KP_MULTIPLY", 57412: "KP_SUBTRACT", 57413: "KP_ADD", 57414: "KP_ENTER", 57415: "KP_EQUAL", 57416: "KP_SEPARATOR", 57417: "KP_LEFT", 57418: "KP_RIGHT", 57419: "KP_UP", 57420: "KP_DOWN", 57421: "KP_PAGE_UP", 57422: "KP_PAGE_DOWN", 57423: "KP_HOME", 57424: "KP_END", 57425: "KP_INSERT", 57426: "KP_DELETE", 57427: "KP_BEGIN", 57428: "MEDIA_PLAY", 57429: "MEDIA_PAUSE", 57430: "MEDIA_PLAY_PAUSE", 57431: "MEDIA_REVERSE", 57432: "MEDIA_STOP", 57433: "MEDIA_FAST_FORWARD", 57434: "MEDIA_REWIND", 57435: "MEDIA_TRACK_NEXT", 57436: "MEDIA_TRACK_PREVIOUS", 57437: "MEDIA_RECORD", 57438: "LOWER_VOLUME", 57439: "RAISE_VOLUME", 57440: "MUTE_VOLUME", 57441: "LEFT_SHIFT", 57442: "LEFT_CONTROL", 57443: "LEFT_ALT", 57444: "LEFT_SUPER", 57445: "LEFT_HYPER", 57446: "LEFT_META", 57447: "RIGHT_SHIFT", 57448: "RIGHT_CONTROL", 57449: "RIGHT_ALT", 57450: "RIGHT_SUPER", 57451: "RIGHT_HYPER", 57452: "RIGHT_META", 57453: "ISO_LEVEL3_SHIFT", 57454: "ISO_LEVEL5_SHIFT"}
@ -10,3 +17,202 @@ var letter_trailer_to_csi_number_map = map[string]int{"A": 57352, "B": 57353, "C
// end csi mapping // end csi mapping
// }}} // }}}
var name_to_functional_number_map map[string]int
type KeyEventType uint8
type KeyModifiers uint16
const (
PRESS KeyEventType = 1
REPEAT KeyEventType = 2
RELEASE KeyEventType = 4
)
const (
SHIFT KeyModifiers = 1
ALT KeyModifiers = 2
CTRL KeyModifiers = 4
SUPER KeyModifiers = 8
HYPER KeyModifiers = 16
META KeyModifiers = 32
CAPS_LOCK KeyModifiers = 64
NUM_LOCK KeyModifiers = 128
)
func (self *KeyModifiers) WithoutLocks() KeyModifiers {
return *self & ^(CAPS_LOCK | NUM_LOCK)
}
type KeyEvent struct {
Type KeyEventType
Mods KeyModifiers
Key string
ShiftedKey string
AlternateKey string
Text string
}
func KeyEventFromCSI(csi string) *KeyEvent {
if len(csi) == 0 {
return nil
}
last_char := csi[len(csi)-1:]
if !strings.Contains("u~ABCDEHFPQRS", last_char) || (last_char == "~" && (csi == "200~" || csi == "201~")) {
return nil
}
csi = csi[:len(csi)-1]
sections := strings.Split(csi, ";")
get_sub_sections := func(section string) []int {
p := strings.Split(section, ":")
ans := make([]int, len(p))
for i, x := range p {
q, err := strconv.Atoi(x)
if err != nil {
return nil
}
ans[i] = q
}
return ans
}
first_section := get_sub_sections(sections[0])
second_section := make([]int, 0)
third_section := make([]int, 0)
if len(sections) > 1 {
second_section = get_sub_sections(sections[1])
}
if len(sections) > 2 {
third_section = get_sub_sections(sections[2])
}
var ans KeyEvent
keynum := first_section[0]
if val, ok := letter_trailer_to_csi_number_map[last_char]; ok {
keynum = val
}
key_name := func(keynum int) string {
switch keynum {
case 0:
return ""
case 13:
if last_char == "u" {
return "ENTER"
}
return "F3"
default:
if val, ok := csi_number_to_functional_number_map[keynum]; ok {
keynum = val
}
ans := ""
if val, ok := functional_key_number_to_name_map[keynum]; ok {
ans = val
} else {
ans = string(rune(keynum))
}
return ans
}
}
ans.Key = key_name(keynum)
if len(first_section) > 1 {
ans.ShiftedKey = key_name(first_section[1])
}
if len(first_section) > 2 {
ans.AlternateKey = key_name(first_section[2])
}
if len(second_section) > 0 {
ans.Mods = KeyModifiers(second_section[0] - 1)
}
if len(second_section) > 1 {
switch second_section[1] {
case 2:
ans.Type = REPEAT
case 3:
ans.Type = RELEASE
}
}
if len(third_section) > 0 {
runes := make([]rune, len(third_section))
for i, ch := range third_section {
runes[i] = rune(ch)
}
ans.Text = string(runes)
}
return &ans
}
type ParsedShortcut struct {
Mods KeyModifiers
KeyName string
}
var parsed_shortcut_cache map[string]ParsedShortcut
func ParseShortcut(spec string) *ParsedShortcut {
if val, ok := parsed_shortcut_cache[spec]; ok {
return &val
}
ospec := spec
if strings.HasSuffix(spec, "+") {
ospec = spec[:len(spec)-1] + "plus"
}
parts := strings.Split(ospec, "+")
key_name := parts[len(parts)-1]
if val, ok := kitty.FunctionalKeyNameAliases[strings.ToUpper(key_name)]; ok {
key_name = val
}
if _, is_functional_key := name_to_functional_number_map[strings.ToUpper(key_name)]; is_functional_key {
key_name = strings.ToUpper(key_name)
} else {
if val, ok := kitty.CharacterKeyNameAliases[strings.ToUpper(key_name)]; ok {
key_name = val
}
}
ans := ParsedShortcut{KeyName: key_name}
parsed_shortcut_cache[spec] = ans
if len(parts) > 1 {
for _, q := range parts[:len(parts)-1] {
val, ok := kitty.ConfigModMap[strings.ToUpper(q)]
if ok {
ans.Mods |= KeyModifiers(val)
} else {
ans.Mods |= META << 8
}
}
}
return &ans
}
func (self *KeyEvent) MatchesParsedShortcut(ps *ParsedShortcut, event_type KeyEventType) bool {
if self.Type&event_type == 0 {
return false
}
mods := self.Mods.WithoutLocks()
if mods == ps.Mods && self.Key == ps.KeyName {
return true
}
if self.ShiftedKey != "" && mods&SHIFT != 0 && (mods & ^SHIFT) == ps.Mods && self.ShiftedKey == ps.KeyName {
return true
}
return false
}
func (self *KeyEvent) Matches(spec string, event_type KeyEventType) bool {
return self.MatchesParsedShortcut(ParseShortcut(spec), event_type)
}
func (self *KeyEvent) MatchesPressOrRepeat(spec string) bool {
return self.MatchesParsedShortcut(ParseShortcut(spec), PRESS|REPEAT)
}
func (self *KeyEvent) MatchesRelease(spec string) bool {
return self.MatchesParsedShortcut(ParseShortcut(spec), RELEASE)
}
func init() {
name_to_functional_number_map = make(map[string]int, len(functional_key_number_to_name_map))
for k, v := range functional_key_number_to_name_map {
name_to_functional_number_map[v] = k
}
}