vim9script # Script to get various codes that keys send, depending on the protocol used. # # Usage: vim -u NONE -S keycode_check.vim # # Author: Bram Moolenaar # Last Update: 2022 Nov 15 # # The codes are stored in the file "keycode_check.json", so that you can # compare the results of various terminals. # # You can select what protocol to enable: # - None # - modifyOtherKeys level 2 # - kitty keyboard protocol # Change directory to where this script is, so that the json file is found # there. exe 'cd ' .. expand(':h') echo 'working in directory: ' .. getcwd() const filename = 'keycode_check.json' # Dictionary of dictionaries with the results in the form: # {'xterm': {protocol: 'none', 'Tab': '09', 'S-Tab': '09'}, # 'xterm2': {protocol: 'mok2', 'Tab': '09', 'S-Tab': '09'}, # 'kitty': {protocol: 'kitty', 'Tab': '09', 'S-Tab': '09'}, # } # The values are in hex form. var keycodes = {} if filereadable(filename) keycodes = readfile(filename)->join()->json_decode() else # Use some dummy entries to try out with keycodes = { 'xterm': {protocol: 'none', 'Tab': '09', 'S-Tab': '09'}, 'kitty': {protocol: 'kitty', 'Tab': '09', 'S-Tab': '1b5b393b3275'}, } endif var orig_keycodes = deepcopy(keycodes) # used to detect something changed # Write the "keycodes" variable in JSON form to "filename". def WriteKeycodes() # If the file already exists move it to become the backup file. if filereadable(filename) if rename(filename, filename .. '~') echoerr $'Renaming {filename} to {filename}~ failed!' return endif endif if writefile([json_encode(keycodes)], filename) != 0 echoerr $'Writing {filename} failed!' endif enddef # The key entries that we want to list, in this order. # The first item is displayed in the prompt, the second is the key in # the keycodes dictionary. var key_entries = [ ['Tab', 'Tab'], ['Shift-Tab', 'S-Tab'], ['Ctrl-Tab', 'C-Tab'], ['Alt-Tab', 'A-Tab'], ['Ctrl-I', 'C-I'], ['Shift-Ctrl-I', 'S-C-I'], ['Esc', 'Esc'], ['Shift-Esc', 'S-Esc'], ['Ctrl-Esc', 'C-Esc'], ['Alt-Esc', 'A-Esc'], ['Space', 'Space'], ['Shift-Space', 'S-Space'], ['Ctrl-Space', 'C-Space'], ['Alt-Space', 'A-Space'], ] # Given a terminal name and a item name, return the text to display. def GetItemDisplay(term: string, item: string): string var val = get(keycodes[term], item, '') # see if we can pretty-print this one var pretty = val if val[0 : 1] == '1b' pretty = 'ESC' var idx = 2 if val[0 : 3] == '1b5b' pretty = 'CSI' idx = 4 endif var digits = false while idx < len(val) var cc = val[idx : idx + 1] var nr = str2nr('0x' .. cc, 16) idx += 2 if nr >= char2nr('0') && nr <= char2nr('9') if !digits pretty ..= ' ' endif digits = true pretty ..= cc[1] else if nr == char2nr(';') && digits # don't use space between semicolon and digits to keep it short pretty ..= ';' else digits = false if nr >= char2nr(' ') && nr <= char2nr('~') # printable character pretty ..= ' ' .. printf('%c', nr) else # non-printable, use hex code pretty = val break endif endif endif endwhile endif return pretty enddef # Action: list the information in "keycodes" in a more or less nice way. def ActionList() var terms = keys(keycodes) if len(terms) == 0 echo 'No terminal results yet' return endif sort(terms) var items = ['protocol', 'version', 'kitty', 'modkeys'] + key_entries->copy()->map((_, v) => v[1]) # For each terminal compute the needed width, add two. # You may need to increase the terminal width to avoid wrapping. var widths = [] for [idx, term] in items(terms) widths[idx] = len(term) + 2 endfor for item in items for [idx, term] in items(terms) var l = len(GetItemDisplay(term, item)) if widths[idx] < l + 2 widths[idx] = l + 2 endif endfor endfor # Use one column of width 10 for the item name. echo "\n" echon ' ' for [idx, term] in items(terms) echon printf('%-' .. widths[idx] .. 's', term) endfor echo "\n" for item in items echon printf('%8s ', item) for [idx, term] in items(terms) echon printf('%-' .. widths[idx] .. 's', GetItemDisplay(term, item)) endfor echo '' endfor echo "\n" enddef # Convert the literal string after "raw key input" into hex form. def Literal2hex(code: string): string var hex = '' for i in range(len(code)) hex ..= printf('%02x', char2nr(code[i])) endfor return hex enddef def GetTermName(): string var name = input('Enter the name of the terminal: ') return name enddef # Gather key codes for terminal "name". def DoTerm(name: string) var proto = inputlist([$'What protocol to enable for {name}:', '1. None', '2. modifyOtherKeys level 2', '3. kitty', ]) echo "\n" &t_TE = "\[>4;m" var proto_name = 'unknown' if proto == 1 # Request the XTQMODKEYS value and request the kitty keyboard protocol status. &t_TI = "\[?4m" .. "\[?u" proto_name = 'none' elseif proto == 2 # Enable modifyOtherKeys level 2 and request the XTQMODKEYS value. &t_TI = "\[>4;2m" .. "\[?4m" proto_name = 'mok2' elseif proto == 3 # Enable Kitty keyboard protocol and request the status. &t_TI = "\[>1u" .. "\[?u" proto_name = 'kitty' else echoerr 'invalid protocol choice' return endif # Append the request for the version response, this is used to check we have # the results. &t_TI ..= "\[>c" # Pattern that matches the line with the version response. const version_pattern = "\\\[>\\d\\+;\\d\\+;\\d*c" # Pattern that matches the XTQMODKEYS response: # CSI > 4;Pv m # where Pv indicates the modifyOtherKeys level const modkeys_pattern = "\\\[>4;\\dm" # Pattern that matches the line with the status. Currently what terminals # return for the Kitty keyboard protocol. const kitty_status_pattern = "\\\[?\\d\\+u" ch_logfile('keylog', 'w') # executing a dummy shell command will output t_TI !echo >/dev/null # Wait until the log file has the version response. var startTime = reltime() var seenVersion = false while !seenVersion var log = readfile('keylog') if len(log) > 2 for line in log if line =~ 'raw key input' var code = substitute(line, '.*raw key input: "\([^"]*\).*', '\1', '') if code =~ version_pattern seenVersion = true echo 'Found the version response' break endif endif endfor endif if reltime(startTime)->reltimefloat() > 3 # break out after three seconds break endif endwhile echo 'seenVersion: ' seenVersion # Prepare the terminal entry, set protocol and clear status and version. if !has_key(keycodes, name) keycodes[name] = {} endif keycodes[name]['protocol'] = proto_name keycodes[name]['version'] = '' keycodes[name]['kitty'] = '' keycodes[name]['modkeys'] = '' # Check the log file for a status and the version response ch_logfile('', '') var log = readfile('keylog') delete('keylog') for line in log if line =~ 'raw key input' var code = substitute(line, '.*raw key input: "\([^"]*\).*', '\1', '') # Check for the XTQMODKEYS response. if code =~ modkeys_pattern var modkeys = substitute(code, '.*\(' .. modkeys_pattern .. '\).*', '\1', '') # We could get the level out of the response, but showing the response # itself provides more information. # modkeys = substitute(modkeys, '.*4;\(\d\)m', '\1', '') if keycodes[name]['modkeys'] != '' echomsg 'Another modkeys found after ' .. keycodes[name]['modkeys'] endif keycodes[name]['modkeys'] = modkeys endif # Check for kitty keyboard protocol status if code =~ kitty_status_pattern var status = substitute(code, '.*\(' .. kitty_status_pattern .. '\).*', '\1', '') # use the response itself as the status status = Literal2hex(status) if keycodes[name]['kitty'] != '' echomsg 'Another status found after ' .. keycodes[name]['kitty'] endif keycodes[name]['kitty'] = status endif if code =~ version_pattern var version = substitute(code, '.*\(' .. version_pattern .. '\).*', '\1', '') keycodes[name]['version'] = Literal2hex(version) break endif endif endfor echo "For Alt to work you may need to press the Windows/Super key as well" echo "When a key press doesn't get to Vim (e.g. when using Alt) press x" # The log of ignored typeahead is left around for debugging, start with an # empty file here. delete('keylog-ignore') for entry in key_entries # Consume any typeahead. Wait a bit for any responses to arrive. ch_logfile('keylog-ignore', 'a') while 1 sleep 100m if getchar(1) == 0 break endif while getchar(1) != 0 getchar() endwhile endwhile ch_logfile('', '') ch_logfile('keylog', 'w') echo $'Press the {entry[0]} key (q to quit):' var r = getcharstr() ch_logfile('', '') if r == 'q' break endif log = readfile('keylog') delete('keylog') if len(log) < 2 echoerr 'failed to read result' return endif var done = false for line in log if line =~ 'raw key input' var code = substitute(line, '.*raw key input: "\([^"]*\).*', '\1', '') # Remove any version termresponse code = substitute(code, version_pattern, '', 'g') # Remove any XTGETTCAP replies. const cappat = "\P[01]+\\k\\+=\\x*\\\\\" code = substitute(code, cappat, '', 'g') # Remove any kitty status reply code = substitute(code, kitty_status_pattern, '', 'g') if code == '' continue endif # Convert the literal bytes into hex. If 'x' was pressed then clear # the entry. var hex = '' if code != 'x' hex = Literal2hex(code) endif keycodes[name][entry[1]] = hex done = true break endif endfor if !done echo 'Code not found in log' endif endfor enddef # Action: Add key codes for a new terminal. def ActionAdd() var name = input('Enter name of the terminal: ') echo "\n" if index(keys(keycodes), name) >= 0 echoerr $'Terminal {name} already exists' return endif DoTerm(name) enddef # Action: Replace key codes for an already known terminal. def ActionReplace() var terms = keys(keycodes) if len(terms) == 0 echo 'No terminal results yet' return endif var choice = inputlist(['Select:'] + terms->copy()->map((idx, arg) => (idx + 1) .. ': ' .. arg)) echo "\n" if choice > 0 && choice <= len(terms) DoTerm(terms[choice - 1]) else echo 'invalid index' endif enddef # Action: Clear key codes for an already known terminal. def ActionClear() var terms = keys(keycodes) if len(terms) == 0 echo 'No terminal results yet' return endif var choice = inputlist(['Select:'] + terms->copy()->map((idx, arg) => (idx + 1) .. ': ' .. arg)) echo "\n" if choice > 0 && choice <= len(terms) remove(keycodes, terms[choice - 1]) else echo 'invalid index' endif enddef # Action: Quit, possibly after saving the results first. def ActionQuit() # If nothing was changed just quit if keycodes == orig_keycodes quit endif while true var res = input("Save the changed key codes (y/n)? ") if res == 'n' quit endif if res == 'y' WriteKeycodes() quit endif echo 'invalid reply' endwhile enddef # The main loop while true var action = inputlist(['Select operation:', '1. List results', '2. Add results for a new terminal', '3. Replace results', '4. Clear results', '5. Quit', ]) echo "\n" if action == 1 ActionList() elseif action == 2 ActionAdd() elseif action == 3 ActionReplace() elseif action == 4 ActionClear() elseif action == 5 ActionQuit() endif endwhile