blob: ca759ca50443e09507ea997d30ffb90090795a8a [file] [log] [blame]
Bram Moolenaara44c7812022-11-15 22:59:07 +00001vim9script
2
3# Script to get various codes that keys send, depending on the protocol used.
4#
Bram Moolenaar83030352022-11-16 16:08:30 +00005# Usage: vim -u NONE -S keycode_check.vim
Bram Moolenaara44c7812022-11-15 22:59:07 +00006#
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.
20exe 'cd ' .. expand('<sfile>:h')
21echo 'working in directory: ' .. getcwd()
22
23const 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.
31var keycodes = {}
32
33if filereadable(filename)
34 keycodes = readfile(filename)->join()->json_decode()
35else
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 }
41endif
42var orig_keycodes = deepcopy(keycodes) # used to detect something changed
43
44# Write the "keycodes" variable in JSON form to "filename".
45def 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
57enddef
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.
62var 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 Moolenaar83030352022-11-16 16:08:30 +000079# Given a terminal name and a item name, return the text to display.
80def 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
125enddef
126
Bram Moolenaara44c7812022-11-15 22:59:07 +0000127
128# Action: list the information in "keycodes" in a more or less nice way.
129def ActionList()
130 var terms = keys(keycodes)
131 if len(terms) == 0
132 echo 'No terminal results yet'
133 return
134 endif
Bram Moolenaar83030352022-11-16 16:08:30 +0000135 sort(terms)
Bram Moolenaara44c7812022-11-15 22:59:07 +0000136
Bram Moolenaarc255b782022-11-26 19:16:48 +0000137 var items = ['protocol', 'version', 'status', 'modkeys']
Bram Moolenaar83030352022-11-16 16:08:30 +0000138 + 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 Moolenaara44c7812022-11-15 22:59:07 +0000161 endfor
162 echo "\n"
163
Bram Moolenaara44c7812022-11-15 22:59:07 +0000164 for item in items
165 echon printf('%8s ', item)
Bram Moolenaar83030352022-11-16 16:08:30 +0000166 for [idx, term] in items(terms)
167 echon printf('%-' .. widths[idx] .. 's', GetItemDisplay(term, item))
Bram Moolenaara44c7812022-11-15 22:59:07 +0000168 endfor
169 echo ''
170 endfor
171 echo "\n"
172enddef
173
Bram Moolenaar83030352022-11-16 16:08:30 +0000174# Convert the literal string after "raw key input" into hex form.
175def 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
181enddef
182
Bram Moolenaara44c7812022-11-15 22:59:07 +0000183def GetTermName(): string
184 var name = input('Enter the name of the terminal: ')
185 return name
186enddef
187
188# Gather key codes for terminal "name".
189def 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 Moolenaarc255b782022-11-26 19:16:48 +0000201 # Enable modifyOtherKeys level 2. Request the XTQMODKEYS value.
202 &t_TI = "\<Esc>[>4;2m" .. "\<Esc>[?4m"
Bram Moolenaara44c7812022-11-15 22:59:07 +0000203 proto_name = 'mok2'
204 elseif proto == 3
Bram Moolenaar83030352022-11-16 16:08:30 +0000205 # Enable Kitty keyboard protocol and request the status
206 &t_TI = "\<Esc>[>1u" .. "\<Esc>[?u"
Bram Moolenaara44c7812022-11-15 22:59:07 +0000207 proto_name = 'kitty'
208 else
209 echoerr 'invalid protocol choice'
210 return
211 endif
212
Bram Moolenaar83030352022-11-16 16:08:30 +0000213 # 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
Bram Moolenaarc255b782022-11-26 19:16:48 +0000220 # Pattern that matches the XTQMODKEYS response:
221 # CSI > 4;Pv m
222 # where Pv indicates the modifyOtherKeys level
223 const modkeys_pattern = "\<Esc>\\[>4;\\dm"
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000224
Bram Moolenaar83030352022-11-16 16:08:30 +0000225 # Pattern that matches the line with the status. Currently what terminals
226 # return for the Kitty keyboard protocol.
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000227 const kitty_status_pattern = "\<Esc>\\[?\\d\\+u"
Bram Moolenaar83030352022-11-16 16:08:30 +0000228
229 ch_logfile('keylog', 'w')
230
Bram Moolenaara44c7812022-11-15 22:59:07 +0000231 # executing a dummy shell command will output t_TI
232 !echo >/dev/null
233
Bram Moolenaar83030352022-11-16 16:08:30 +0000234 # Wait until the log file has the version response.
235 var startTime = reltime()
236 var seenVersion = false
237 while !seenVersion
238 var log = readfile('keylog')
239 if len(log) > 2
240 for line in log
241 if line =~ 'raw key input'
242 var code = substitute(line, '.*raw key input: "\([^"]*\).*', '\1', '')
243 if code =~ version_pattern
244 seenVersion = true
245 echo 'Found the version response'
246 break
247 endif
248 endif
249 endfor
250 endif
251 if reltime(startTime)->reltimefloat() > 3
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000252 # break out after three seconds
Bram Moolenaar83030352022-11-16 16:08:30 +0000253 break
254 endif
255 endwhile
256
257 echo 'seenVersion: ' seenVersion
258
259 # Prepare the terminal entry, set protocol and clear status and version.
Bram Moolenaara44c7812022-11-15 22:59:07 +0000260 if !has_key(keycodes, name)
261 keycodes[name] = {}
262 endif
263 keycodes[name]['protocol'] = proto_name
Bram Moolenaar83030352022-11-16 16:08:30 +0000264 keycodes[name]['version'] = ''
265 keycodes[name]['status'] = ''
Bram Moolenaarc255b782022-11-26 19:16:48 +0000266 keycodes[name]['modkeys'] = ''
Bram Moolenaara44c7812022-11-15 22:59:07 +0000267
Bram Moolenaar83030352022-11-16 16:08:30 +0000268 # Check the log file for a status and the version response
269 ch_logfile('', '')
270 var log = readfile('keylog')
271 delete('keylog')
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000272
Bram Moolenaar83030352022-11-16 16:08:30 +0000273 for line in log
274 if line =~ 'raw key input'
275 var code = substitute(line, '.*raw key input: "\([^"]*\).*', '\1', '')
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000276
Bram Moolenaarc255b782022-11-26 19:16:48 +0000277 # Check for the XTQMODKEYS response.
278 if code =~ modkeys_pattern
279 var modkeys = substitute(code, '.*\(' .. modkeys_pattern .. '\).*', '\1', '')
280 # Get the level out of the response
281 modkeys = substitute(modkeys, '.*4;\(\d\)m', '\1', '')
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000282
Bram Moolenaarc255b782022-11-26 19:16:48 +0000283 if keycodes[name]['modkeys'] != ''
284 echomsg 'Another modkeys found after ' .. keycodes[name]['modkeys']
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000285 endif
Bram Moolenaarc255b782022-11-26 19:16:48 +0000286 keycodes[name]['modkeys'] = modkeys
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000287 endif
288
Bram Moolenaar83030352022-11-16 16:08:30 +0000289 # Check for kitty keyboard protocol status
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000290 if code =~ kitty_status_pattern
291 var status = substitute(code, '.*\(' .. kitty_status_pattern .. '\).*', '\1', '')
292 # use the response itself as the status
293 status = Literal2hex(status)
294
295 if keycodes[name]['status'] != ''
296 echomsg 'Another status found after ' .. keycodes[name]['status']
297 endif
298 keycodes[name]['status'] = status
Bram Moolenaar83030352022-11-16 16:08:30 +0000299 endif
300
301 if code =~ version_pattern
302 var version = substitute(code, '.*\(' .. version_pattern .. '\).*', '\1', '')
303 keycodes[name]['version'] = Literal2hex(version)
304 break
305 endif
306 endif
307 endfor
308
309 echo "For Alt to work you may need to press the Windows/Super key as well"
310 echo "When a key press doesn't get to Vim (e.g. when using Alt) press x"
Bram Moolenaara44c7812022-11-15 22:59:07 +0000311
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000312 # The log of ignored typeahead is left around for debugging, start with an
313 # empty file here.
314 delete('keylog-ignore')
315
Bram Moolenaara44c7812022-11-15 22:59:07 +0000316 for entry in key_entries
Bram Moolenaar83030352022-11-16 16:08:30 +0000317 # Consume any typeahead. Wait a bit for any responses to arrive.
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000318 ch_logfile('keylog-ignore', 'a')
319 while 1
Bram Moolenaar83030352022-11-16 16:08:30 +0000320 sleep 100m
Bram Moolenaarc896adb2022-11-19 19:02:40 +0000321 if getchar(1) == 0
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000322 break
323 endif
Bram Moolenaarc896adb2022-11-19 19:02:40 +0000324 while getchar(1) != 0
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000325 getchar()
326 endwhile
Bram Moolenaar83030352022-11-16 16:08:30 +0000327 endwhile
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000328 ch_logfile('', '')
Bram Moolenaar83030352022-11-16 16:08:30 +0000329
Bram Moolenaara44c7812022-11-15 22:59:07 +0000330 ch_logfile('keylog', 'w')
331 echo $'Press the {entry[0]} key (q to quit):'
332 var r = getcharstr()
333 ch_logfile('', '')
334 if r == 'q'
335 break
336 endif
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000337
Bram Moolenaar83030352022-11-16 16:08:30 +0000338 log = readfile('keylog')
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000339 delete('keylog')
Bram Moolenaara44c7812022-11-15 22:59:07 +0000340 if len(log) < 2
341 echoerr 'failed to read result'
342 return
343 endif
344 var done = false
345 for line in log
346 if line =~ 'raw key input'
347 var code = substitute(line, '.*raw key input: "\([^"]*\).*', '\1', '')
348
Bram Moolenaar83030352022-11-16 16:08:30 +0000349 # Remove any version termresponse
350 code = substitute(code, version_pattern, '', 'g')
351
352 # Remove any XTGETTCAP replies.
353 const cappat = "\<Esc>P[01]+\\k\\+=\\x*\<Esc>\\\\"
354 code = substitute(code, cappat, '', 'g')
355
356 # Remove any kitty status reply
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000357 code = substitute(code, kitty_status_pattern, '', 'g')
Bram Moolenaar83030352022-11-16 16:08:30 +0000358 if code == ''
359 continue
360 endif
361
362 # Convert the literal bytes into hex. If 'x' was pressed then clear
363 # the entry.
Bram Moolenaara44c7812022-11-15 22:59:07 +0000364 var hex = ''
Bram Moolenaar83030352022-11-16 16:08:30 +0000365 if code != 'x'
366 hex = Literal2hex(code)
367 endif
368
Bram Moolenaara44c7812022-11-15 22:59:07 +0000369 keycodes[name][entry[1]] = hex
370 done = true
371 break
372 endif
373 endfor
374 if !done
375 echo 'Code not found in log'
376 endif
377 endfor
378enddef
379
380# Action: Add key codes for a new terminal.
381def ActionAdd()
382 var name = input('Enter name of the terminal: ')
383 echo "\n"
384 if index(keys(keycodes), name) >= 0
385 echoerr $'Terminal {name} already exists'
386 return
387 endif
388
389 DoTerm(name)
390enddef
391
392# Action: Replace key codes for an already known terminal.
393def ActionReplace()
394 var terms = keys(keycodes)
395 if len(terms) == 0
396 echo 'No terminal results yet'
397 return
398 endif
399
400 var choice = inputlist(['Select:'] + terms->copy()->map((idx, arg) => (idx + 1) .. ': ' .. arg))
401 echo "\n"
402 if choice > 0 && choice <= len(terms)
403 DoTerm(terms[choice - 1])
Bram Moolenaar83030352022-11-16 16:08:30 +0000404 else
405 echo 'invalid index'
Bram Moolenaara44c7812022-11-15 22:59:07 +0000406 endif
Bram Moolenaar83030352022-11-16 16:08:30 +0000407enddef
408
409# Action: Clear key codes for an already known terminal.
410def ActionClear()
411 var terms = keys(keycodes)
412 if len(terms) == 0
413 echo 'No terminal results yet'
414 return
415 endif
416
417 var choice = inputlist(['Select:'] + terms->copy()->map((idx, arg) => (idx + 1) .. ': ' .. arg))
418 echo "\n"
419 if choice > 0 && choice <= len(terms)
420 remove(keycodes, terms[choice - 1])
421 else
422 echo 'invalid index'
423 endif
Bram Moolenaara44c7812022-11-15 22:59:07 +0000424enddef
425
426# Action: Quit, possibly after saving the results first.
427def ActionQuit()
428 # If nothing was changed just quit
429 if keycodes == orig_keycodes
430 quit
431 endif
432
433 while true
434 var res = input("Save the changed key codes (y/n)? ")
435 if res == 'n'
436 quit
437 endif
438 if res == 'y'
439 WriteKeycodes()
440 quit
441 endif
442 echo 'invalid reply'
443 endwhile
444enddef
445
446# The main loop
447while true
448 var action = inputlist(['Select operation:',
449 '1. List results',
450 '2. Add results for a new terminal',
451 '3. Replace results',
Bram Moolenaar83030352022-11-16 16:08:30 +0000452 '4. Clear results',
453 '5. Quit',
Bram Moolenaara44c7812022-11-15 22:59:07 +0000454 ])
455 echo "\n"
456 if action == 1
457 ActionList()
458 elseif action == 2
459 ActionAdd()
460 elseif action == 3
461 ActionReplace()
462 elseif action == 4
Bram Moolenaar83030352022-11-16 16:08:30 +0000463 ActionClear()
464 elseif action == 5
Bram Moolenaara44c7812022-11-15 22:59:07 +0000465 ActionQuit()
466 endif
467endwhile