diff options
Diffstat (limited to 'src/testdir/keycode_check.vim')
-rw-r--r-- | src/testdir/keycode_check.vim | 470 |
1 files changed, 470 insertions, 0 deletions
diff --git a/src/testdir/keycode_check.vim b/src/testdir/keycode_check.vim new file mode 100644 index 0000000..8320341 --- /dev/null +++ b/src/testdir/keycode_check.vim @@ -0,0 +1,470 @@ +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('<sfile>: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 = "\<Esc>[>4;m" + var proto_name = 'unknown' + if proto == 1 + # Request the XTQMODKEYS value and request the kitty keyboard protocol status. + &t_TI = "\<Esc>[?4m" .. "\<Esc>[?u" + proto_name = 'none' + elseif proto == 2 + # Enable modifyOtherKeys level 2 and request the XTQMODKEYS value. + &t_TI = "\<Esc>[>4;2m" .. "\<Esc>[?4m" + proto_name = 'mok2' + elseif proto == 3 + # Enable Kitty keyboard protocol and request the status. + &t_TI = "\<Esc>[>1u" .. "\<Esc>[?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 ..= "\<Esc>[>c" + + # Pattern that matches the line with the version response. + const version_pattern = "\<Esc>\\[>\\d\\+;\\d\\+;\\d*c" + + # Pattern that matches the XTQMODKEYS response: + # CSI > 4;Pv m + # where Pv indicates the modifyOtherKeys level + const modkeys_pattern = "\<Esc>\\[>4;\\dm" + + # Pattern that matches the line with the status. Currently what terminals + # return for the Kitty keyboard protocol. + const kitty_status_pattern = "\<Esc>\\[?\\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 = "\<Esc>P[01]+\\k\\+=\\x*\<Esc>\\\\" + 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 |