Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 1 | vim9script |
| 2 | |
| 3 | # Script to get various codes that keys send, depending on the protocol used. |
| 4 | # |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 5 | # Usage: vim -u NONE -S keycode_check.vim |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 6 | # |
| 7 | # Author: Bram Moolenaar |
| 8 | # Last Update: 2022 Nov 15 |
| 9 | # |
| 10 | # The codes are stored in the file "keycode_check.json", so that you can |
| 11 | # compare the results of various terminals. |
| 12 | # |
| 13 | # You can select what protocol to enable: |
| 14 | # - None |
| 15 | # - modifyOtherKeys level 2 |
| 16 | # - kitty keyboard protocol |
| 17 | |
| 18 | # Change directory to where this script is, so that the json file is found |
| 19 | # there. |
| 20 | exe 'cd ' .. expand('<sfile>:h') |
| 21 | echo 'working in directory: ' .. getcwd() |
| 22 | |
| 23 | const filename = 'keycode_check.json' |
| 24 | |
| 25 | # Dictionary of dictionaries with the results in the form: |
| 26 | # {'xterm': {protocol: 'none', 'Tab': '09', 'S-Tab': '09'}, |
| 27 | # 'xterm2': {protocol: 'mok2', 'Tab': '09', 'S-Tab': '09'}, |
| 28 | # 'kitty': {protocol: 'kitty', 'Tab': '09', 'S-Tab': '09'}, |
| 29 | # } |
| 30 | # The values are in hex form. |
| 31 | var keycodes = {} |
| 32 | |
| 33 | if filereadable(filename) |
| 34 | keycodes = readfile(filename)->join()->json_decode() |
| 35 | else |
| 36 | # Use some dummy entries to try out with |
| 37 | keycodes = { |
| 38 | 'xterm': {protocol: 'none', 'Tab': '09', 'S-Tab': '09'}, |
| 39 | 'kitty': {protocol: 'kitty', 'Tab': '09', 'S-Tab': '1b5b393b3275'}, |
| 40 | } |
| 41 | endif |
| 42 | var orig_keycodes = deepcopy(keycodes) # used to detect something changed |
| 43 | |
| 44 | # Write the "keycodes" variable in JSON form to "filename". |
| 45 | def WriteKeycodes() |
| 46 | # If the file already exists move it to become the backup file. |
| 47 | if filereadable(filename) |
| 48 | if rename(filename, filename .. '~') |
| 49 | echoerr $'Renaming {filename} to {filename}~ failed!' |
| 50 | return |
| 51 | endif |
| 52 | endif |
| 53 | |
| 54 | if writefile([json_encode(keycodes)], filename) != 0 |
| 55 | echoerr $'Writing {filename} failed!' |
| 56 | endif |
| 57 | enddef |
| 58 | |
| 59 | # The key entries that we want to list, in this order. |
| 60 | # The first item is displayed in the prompt, the second is the key in |
| 61 | # the keycodes dictionary. |
| 62 | var key_entries = [ |
| 63 | ['Tab', 'Tab'], |
| 64 | ['Shift-Tab', 'S-Tab'], |
| 65 | ['Ctrl-Tab', 'C-Tab'], |
| 66 | ['Alt-Tab', 'A-Tab'], |
| 67 | ['Ctrl-I', 'C-I'], |
| 68 | ['Shift-Ctrl-I', 'S-C-I'], |
| 69 | ['Esc', 'Esc'], |
| 70 | ['Shift-Esc', 'S-Esc'], |
| 71 | ['Ctrl-Esc', 'C-Esc'], |
| 72 | ['Alt-Esc', 'A-Esc'], |
| 73 | ['Space', 'Space'], |
| 74 | ['Shift-Space', 'S-Space'], |
| 75 | ['Ctrl-Space', 'C-Space'], |
| 76 | ['Alt-Space', 'A-Space'], |
| 77 | ] |
| 78 | |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 79 | # Given a terminal name and a item name, return the text to display. |
| 80 | def GetItemDisplay(term: string, item: string): string |
| 81 | var val = get(keycodes[term], item, '') |
| 82 | |
| 83 | # see if we can pretty-print this one |
| 84 | var pretty = val |
| 85 | if val[0 : 1] == '1b' |
| 86 | pretty = 'ESC' |
| 87 | var idx = 2 |
| 88 | |
| 89 | if val[0 : 3] == '1b5b' |
| 90 | pretty = 'CSI' |
| 91 | idx = 4 |
| 92 | endif |
| 93 | |
| 94 | var digits = false |
| 95 | while idx < len(val) |
| 96 | var cc = val[idx : idx + 1] |
| 97 | var nr = str2nr('0x' .. cc, 16) |
| 98 | idx += 2 |
| 99 | if nr >= char2nr('0') && nr <= char2nr('9') |
| 100 | if !digits |
| 101 | pretty ..= ' ' |
| 102 | endif |
| 103 | digits = true |
| 104 | pretty ..= cc[1] |
| 105 | else |
| 106 | if nr == char2nr(';') && digits |
| 107 | # don't use space between semicolon and digits to keep it short |
| 108 | pretty ..= ';' |
| 109 | else |
| 110 | digits = false |
| 111 | if nr >= char2nr(' ') && nr <= char2nr('~') |
| 112 | # printable character |
| 113 | pretty ..= ' ' .. printf('%c', nr) |
| 114 | else |
| 115 | # non-printable, use hex code |
| 116 | pretty = val |
| 117 | break |
| 118 | endif |
| 119 | endif |
| 120 | endif |
| 121 | endwhile |
| 122 | endif |
| 123 | |
| 124 | return pretty |
| 125 | enddef |
| 126 | |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 127 | |
| 128 | # Action: list the information in "keycodes" in a more or less nice way. |
| 129 | def ActionList() |
| 130 | var terms = keys(keycodes) |
| 131 | if len(terms) == 0 |
| 132 | echo 'No terminal results yet' |
| 133 | return |
| 134 | endif |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 135 | sort(terms) |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 136 | |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 137 | var items = ['protocol', 'version', 'status'] |
| 138 | + key_entries->copy()->map((_, v) => v[1]) |
| 139 | |
| 140 | # For each terminal compute the needed width, add two. |
| 141 | # You may need to increase the terminal width to avoid wrapping. |
| 142 | var widths = [] |
| 143 | for [idx, term] in items(terms) |
| 144 | widths[idx] = len(term) + 2 |
| 145 | endfor |
| 146 | |
| 147 | for item in items |
| 148 | for [idx, term] in items(terms) |
| 149 | var l = len(GetItemDisplay(term, item)) |
| 150 | if widths[idx] < l + 2 |
| 151 | widths[idx] = l + 2 |
| 152 | endif |
| 153 | endfor |
| 154 | endfor |
| 155 | |
| 156 | # Use one column of width 10 for the item name. |
| 157 | echo "\n" |
| 158 | echon ' ' |
| 159 | for [idx, term] in items(terms) |
| 160 | echon printf('%-' .. widths[idx] .. 's', term) |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 161 | endfor |
| 162 | echo "\n" |
| 163 | |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 164 | for item in items |
| 165 | echon printf('%8s ', item) |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 166 | for [idx, term] in items(terms) |
| 167 | echon printf('%-' .. widths[idx] .. 's', GetItemDisplay(term, item)) |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 168 | endfor |
| 169 | echo '' |
| 170 | endfor |
| 171 | echo "\n" |
| 172 | enddef |
| 173 | |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 174 | # Convert the literal string after "raw key input" into hex form. |
| 175 | def Literal2hex(code: string): string |
| 176 | var hex = '' |
| 177 | for i in range(len(code)) |
| 178 | hex ..= printf('%02x', char2nr(code[i])) |
| 179 | endfor |
| 180 | return hex |
| 181 | enddef |
| 182 | |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 183 | def GetTermName(): string |
| 184 | var name = input('Enter the name of the terminal: ') |
| 185 | return name |
| 186 | enddef |
| 187 | |
| 188 | # Gather key codes for terminal "name". |
| 189 | def DoTerm(name: string) |
| 190 | var proto = inputlist([$'What protocol to enable for {name}:', |
| 191 | '1. None', |
| 192 | '2. modifyOtherKeys level 2', |
| 193 | '3. kitty', |
| 194 | ]) |
| 195 | echo "\n" |
| 196 | &t_TE = "\<Esc>[>4;m" |
| 197 | var proto_name = 'none' |
| 198 | if proto == 1 |
| 199 | &t_TI = "" |
| 200 | elseif proto == 2 |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 201 | # Enable modifyOtherKeys level 2 - no status is reported |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 202 | &t_TI = "\<Esc>[>4;2m" |
| 203 | proto_name = 'mok2' |
| 204 | elseif proto == 3 |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 205 | # Enable Kitty keyboard protocol and request the status |
| 206 | &t_TI = "\<Esc>[>1u" .. "\<Esc>[?u" |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 207 | proto_name = 'kitty' |
| 208 | else |
| 209 | echoerr 'invalid protocol choice' |
| 210 | return |
| 211 | endif |
| 212 | |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 213 | # Append the request for the version response, this is used to check we have |
| 214 | # the results. |
| 215 | &t_TI ..= "\<Esc>[>c" |
| 216 | |
| 217 | # Pattern that matches the line with the version response. |
| 218 | const version_pattern = "\<Esc>\\[>\\d\\+;\\d\\+;\\d*c" |
| 219 | |
| 220 | # Pattern that matches the line with the status. Currently what terminals |
| 221 | # return for the Kitty keyboard protocol. |
| 222 | const status_pattern = "\<Esc>\\[?\\d\\+u" |
| 223 | |
| 224 | ch_logfile('keylog', 'w') |
| 225 | |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 226 | # executing a dummy shell command will output t_TI |
| 227 | !echo >/dev/null |
| 228 | |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 229 | # Wait until the log file has the version response. |
| 230 | var startTime = reltime() |
| 231 | var seenVersion = false |
| 232 | while !seenVersion |
| 233 | var log = readfile('keylog') |
| 234 | if len(log) > 2 |
| 235 | for line in log |
| 236 | if line =~ 'raw key input' |
| 237 | var code = substitute(line, '.*raw key input: "\([^"]*\).*', '\1', '') |
| 238 | if code =~ version_pattern |
| 239 | seenVersion = true |
| 240 | echo 'Found the version response' |
| 241 | break |
| 242 | endif |
| 243 | endif |
| 244 | endfor |
| 245 | endif |
| 246 | if reltime(startTime)->reltimefloat() > 3 |
| 247 | break |
| 248 | endif |
| 249 | endwhile |
| 250 | |
| 251 | echo 'seenVersion: ' seenVersion |
| 252 | |
| 253 | # Prepare the terminal entry, set protocol and clear status and version. |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 254 | if !has_key(keycodes, name) |
| 255 | keycodes[name] = {} |
| 256 | endif |
| 257 | keycodes[name]['protocol'] = proto_name |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 258 | keycodes[name]['version'] = '' |
| 259 | keycodes[name]['status'] = '' |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 260 | |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 261 | # Check the log file for a status and the version response |
| 262 | ch_logfile('', '') |
| 263 | var log = readfile('keylog') |
| 264 | delete('keylog') |
| 265 | for line in log |
| 266 | if line =~ 'raw key input' |
| 267 | var code = substitute(line, '.*raw key input: "\([^"]*\).*', '\1', '') |
| 268 | # Check for kitty keyboard protocol status |
| 269 | if code =~ status_pattern |
| 270 | var status = substitute(code, '.*\(' .. status_pattern .. '\).*', '\1', '') |
| 271 | keycodes[name]['status'] = Literal2hex(status) |
| 272 | endif |
| 273 | |
| 274 | if code =~ version_pattern |
| 275 | var version = substitute(code, '.*\(' .. version_pattern .. '\).*', '\1', '') |
| 276 | keycodes[name]['version'] = Literal2hex(version) |
| 277 | break |
| 278 | endif |
| 279 | endif |
| 280 | endfor |
| 281 | |
| 282 | echo "For Alt to work you may need to press the Windows/Super key as well" |
| 283 | echo "When a key press doesn't get to Vim (e.g. when using Alt) press x" |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 284 | |
| 285 | for entry in key_entries |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 286 | # Consume any typeahead. Wait a bit for any responses to arrive. |
| 287 | sleep 100m |
| 288 | while getchar(1) |
| 289 | getchar() |
| 290 | sleep 100m |
| 291 | endwhile |
| 292 | |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 293 | ch_logfile('keylog', 'w') |
| 294 | echo $'Press the {entry[0]} key (q to quit):' |
| 295 | var r = getcharstr() |
| 296 | ch_logfile('', '') |
| 297 | if r == 'q' |
| 298 | break |
| 299 | endif |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 300 | log = readfile('keylog') |
| 301 | if entry[1] == 'Tab' |
| 302 | # keep a copy |
| 303 | rename('keylog', 'keylog-tab') |
| 304 | else |
| 305 | delete('keylog') |
| 306 | endif |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 307 | if len(log) < 2 |
| 308 | echoerr 'failed to read result' |
| 309 | return |
| 310 | endif |
| 311 | var done = false |
| 312 | for line in log |
| 313 | if line =~ 'raw key input' |
| 314 | var code = substitute(line, '.*raw key input: "\([^"]*\).*', '\1', '') |
| 315 | |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 316 | # Remove any version termresponse |
| 317 | code = substitute(code, version_pattern, '', 'g') |
| 318 | |
| 319 | # Remove any XTGETTCAP replies. |
| 320 | const cappat = "\<Esc>P[01]+\\k\\+=\\x*\<Esc>\\\\" |
| 321 | code = substitute(code, cappat, '', 'g') |
| 322 | |
| 323 | # Remove any kitty status reply |
| 324 | code = substitute(code, status_pattern, '', 'g') |
| 325 | if code == '' |
| 326 | continue |
| 327 | endif |
| 328 | |
| 329 | # Convert the literal bytes into hex. If 'x' was pressed then clear |
| 330 | # the entry. |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 331 | var hex = '' |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 332 | if code != 'x' |
| 333 | hex = Literal2hex(code) |
| 334 | endif |
| 335 | |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 336 | keycodes[name][entry[1]] = hex |
| 337 | done = true |
| 338 | break |
| 339 | endif |
| 340 | endfor |
| 341 | if !done |
| 342 | echo 'Code not found in log' |
| 343 | endif |
| 344 | endfor |
| 345 | enddef |
| 346 | |
| 347 | # Action: Add key codes for a new terminal. |
| 348 | def ActionAdd() |
| 349 | var name = input('Enter name of the terminal: ') |
| 350 | echo "\n" |
| 351 | if index(keys(keycodes), name) >= 0 |
| 352 | echoerr $'Terminal {name} already exists' |
| 353 | return |
| 354 | endif |
| 355 | |
| 356 | DoTerm(name) |
| 357 | enddef |
| 358 | |
| 359 | # Action: Replace key codes for an already known terminal. |
| 360 | def ActionReplace() |
| 361 | var terms = keys(keycodes) |
| 362 | if len(terms) == 0 |
| 363 | echo 'No terminal results yet' |
| 364 | return |
| 365 | endif |
| 366 | |
| 367 | var choice = inputlist(['Select:'] + terms->copy()->map((idx, arg) => (idx + 1) .. ': ' .. arg)) |
| 368 | echo "\n" |
| 369 | if choice > 0 && choice <= len(terms) |
| 370 | DoTerm(terms[choice - 1]) |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 371 | else |
| 372 | echo 'invalid index' |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 373 | endif |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 374 | enddef |
| 375 | |
| 376 | # Action: Clear key codes for an already known terminal. |
| 377 | def ActionClear() |
| 378 | var terms = keys(keycodes) |
| 379 | if len(terms) == 0 |
| 380 | echo 'No terminal results yet' |
| 381 | return |
| 382 | endif |
| 383 | |
| 384 | var choice = inputlist(['Select:'] + terms->copy()->map((idx, arg) => (idx + 1) .. ': ' .. arg)) |
| 385 | echo "\n" |
| 386 | if choice > 0 && choice <= len(terms) |
| 387 | remove(keycodes, terms[choice - 1]) |
| 388 | else |
| 389 | echo 'invalid index' |
| 390 | endif |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 391 | enddef |
| 392 | |
| 393 | # Action: Quit, possibly after saving the results first. |
| 394 | def ActionQuit() |
| 395 | # If nothing was changed just quit |
| 396 | if keycodes == orig_keycodes |
| 397 | quit |
| 398 | endif |
| 399 | |
| 400 | while true |
| 401 | var res = input("Save the changed key codes (y/n)? ") |
| 402 | if res == 'n' |
| 403 | quit |
| 404 | endif |
| 405 | if res == 'y' |
| 406 | WriteKeycodes() |
| 407 | quit |
| 408 | endif |
| 409 | echo 'invalid reply' |
| 410 | endwhile |
| 411 | enddef |
| 412 | |
| 413 | # The main loop |
| 414 | while true |
| 415 | var action = inputlist(['Select operation:', |
| 416 | '1. List results', |
| 417 | '2. Add results for a new terminal', |
| 418 | '3. Replace results', |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 419 | '4. Clear results', |
| 420 | '5. Quit', |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 421 | ]) |
| 422 | echo "\n" |
| 423 | if action == 1 |
| 424 | ActionList() |
| 425 | elseif action == 2 |
| 426 | ActionAdd() |
| 427 | elseif action == 3 |
| 428 | ActionReplace() |
| 429 | elseif action == 4 |
Bram Moolenaar | 8303035 | 2022-11-16 16:08:30 +0000 | [diff] [blame] | 430 | ActionClear() |
| 431 | elseif action == 5 |
Bram Moolenaar | a44c781 | 2022-11-15 22:59:07 +0000 | [diff] [blame] | 432 | ActionQuit() |
| 433 | endif |
| 434 | endwhile |