diff --git a/gen-rc-go.py b/gen-rc-go.py index f8a155ca0..4c52a3797 100755 --- a/gen-rc-go.py +++ b/gen-rc-go.py @@ -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() diff --git a/kitty/key_names.py b/kitty/key_names.py index edda8b252..0b491a176 100644 --- a/kitty/key_names.py +++ b/kitty/key_names.py @@ -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', diff --git a/tools/tui/key-encoding.go b/tools/tui/key-encoding.go index 38a954c11..1b67e9227 100644 --- a/tools/tui/key-encoding.go +++ b/tools/tui/key-encoding.go @@ -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 + } +}