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 subprocess
from typing import Dict, List, Tuple
from typing import Dict, List, Tuple, Union
import kitty.constants as kc
from kittens.tui.operations import Mode
from kitty.cli import OptionDict, OptionSpecSeq, parse_option_spec
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:
return x.replace('\n', '\\n').replace('"', '\\"')
return x.replace('\\', '\\\\').replace('\n', '\\n').replace('"', '\\"')
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
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:
with open('constants_generated.go', 'w') as f:
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]}
var Version VersionType = VersionType{{Major: {kc.version.major}, Minor: {kc.version.minor}, Patch: {kc.version.patch},}}
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:
template = f.read()

View File

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

View File

@ -1,5 +1,12 @@
package tui
import (
"strconv"
"strings"
"kitty"
)
// key encoding mappings {{{
// 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"}
@ -10,3 +17,202 @@ var letter_trailer_to_csi_number_map = map[string]int{"A": 57352, "B": 57353, "C
// 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
}
}