blob: de15180e96a5e2a7bbf0c2647ea2bcd1ffde252f [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 Moolenaar236dffa2022-11-18 21:20:25 +0000137 var items = ['protocol', 'version', 'status', 'resource']
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 Moolenaar236dffa2022-11-18 21:20:25 +0000201 # Enable modifyOtherKeys level 2
202 # Request the resource value: DCS + Q modifyOtherKeys ST
203 &t_TI = "\<Esc>[>4;2m" .. "\<Esc>P+Q6d6f646966794f746865724b657973\<Esc>\\"
Bram Moolenaara44c7812022-11-15 22:59:07 +0000204 proto_name = 'mok2'
205 elseif proto == 3
Bram Moolenaar83030352022-11-16 16:08:30 +0000206 # Enable Kitty keyboard protocol and request the status
207 &t_TI = "\<Esc>[>1u" .. "\<Esc>[?u"
Bram Moolenaara44c7812022-11-15 22:59:07 +0000208 proto_name = 'kitty'
209 else
210 echoerr 'invalid protocol choice'
211 return
212 endif
213
Bram Moolenaar83030352022-11-16 16:08:30 +0000214 # Append the request for the version response, this is used to check we have
215 # the results.
216 &t_TI ..= "\<Esc>[>c"
217
218 # Pattern that matches the line with the version response.
219 const version_pattern = "\<Esc>\\[>\\d\\+;\\d\\+;\\d*c"
220
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000221 # Pattern that matches the resource value response:
222 # DCS 1 + R Pt ST valid
223 # DCS 0 + R Pt ST invalid
224 const resource_pattern = "\<Esc>P[01]+R.*\<Esc>\\\\"
225
Bram Moolenaar83030352022-11-16 16:08:30 +0000226 # Pattern that matches the line with the status. Currently what terminals
227 # return for the Kitty keyboard protocol.
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000228 const kitty_status_pattern = "\<Esc>\\[?\\d\\+u"
Bram Moolenaar83030352022-11-16 16:08:30 +0000229
230 ch_logfile('keylog', 'w')
231
Bram Moolenaara44c7812022-11-15 22:59:07 +0000232 # executing a dummy shell command will output t_TI
233 !echo >/dev/null
234
Bram Moolenaar83030352022-11-16 16:08:30 +0000235 # Wait until the log file has the version response.
236 var startTime = reltime()
237 var seenVersion = false
238 while !seenVersion
239 var log = readfile('keylog')
240 if len(log) > 2
241 for line in log
242 if line =~ 'raw key input'
243 var code = substitute(line, '.*raw key input: "\([^"]*\).*', '\1', '')
244 if code =~ version_pattern
245 seenVersion = true
246 echo 'Found the version response'
247 break
248 endif
249 endif
250 endfor
251 endif
252 if reltime(startTime)->reltimefloat() > 3
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000253 # break out after three seconds
Bram Moolenaar83030352022-11-16 16:08:30 +0000254 break
255 endif
256 endwhile
257
258 echo 'seenVersion: ' seenVersion
259
260 # Prepare the terminal entry, set protocol and clear status and version.
Bram Moolenaara44c7812022-11-15 22:59:07 +0000261 if !has_key(keycodes, name)
262 keycodes[name] = {}
263 endif
264 keycodes[name]['protocol'] = proto_name
Bram Moolenaar83030352022-11-16 16:08:30 +0000265 keycodes[name]['version'] = ''
266 keycodes[name]['status'] = ''
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000267 keycodes[name]['resource'] = ''
Bram Moolenaara44c7812022-11-15 22:59:07 +0000268
Bram Moolenaar83030352022-11-16 16:08:30 +0000269 # Check the log file for a status and the version response
270 ch_logfile('', '')
271 var log = readfile('keylog')
272 delete('keylog')
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000273
Bram Moolenaar83030352022-11-16 16:08:30 +0000274 for line in log
275 if line =~ 'raw key input'
276 var code = substitute(line, '.*raw key input: "\([^"]*\).*', '\1', '')
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000277
278 # Check for resource value response
279 if code =~ resource_pattern
280 var resource = substitute(code, '.*\(' .. resource_pattern .. '\).*', '\1', '')
281 # use the value as the resource, "=30" means zero
282 resource = substitute(resource, '.*\(=\p\+\).*', '\1', '')
283
284 if keycodes[name]['resource'] != ''
285 echomsg 'Another resource found after ' .. keycodes[name]['resource']
286 endif
287 keycodes[name]['resource'] = resource
288 endif
289
Bram Moolenaar83030352022-11-16 16:08:30 +0000290 # Check for kitty keyboard protocol status
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000291 if code =~ kitty_status_pattern
292 var status = substitute(code, '.*\(' .. kitty_status_pattern .. '\).*', '\1', '')
293 # use the response itself as the status
294 status = Literal2hex(status)
295
296 if keycodes[name]['status'] != ''
297 echomsg 'Another status found after ' .. keycodes[name]['status']
298 endif
299 keycodes[name]['status'] = status
Bram Moolenaar83030352022-11-16 16:08:30 +0000300 endif
301
302 if code =~ version_pattern
303 var version = substitute(code, '.*\(' .. version_pattern .. '\).*', '\1', '')
304 keycodes[name]['version'] = Literal2hex(version)
305 break
306 endif
307 endif
308 endfor
309
310 echo "For Alt to work you may need to press the Windows/Super key as well"
311 echo "When a key press doesn't get to Vim (e.g. when using Alt) press x"
Bram Moolenaara44c7812022-11-15 22:59:07 +0000312
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000313 # The log of ignored typeahead is left around for debugging, start with an
314 # empty file here.
315 delete('keylog-ignore')
316
Bram Moolenaara44c7812022-11-15 22:59:07 +0000317 for entry in key_entries
Bram Moolenaar83030352022-11-16 16:08:30 +0000318 # Consume any typeahead. Wait a bit for any responses to arrive.
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000319 ch_logfile('keylog-ignore', 'a')
320 while 1
Bram Moolenaar83030352022-11-16 16:08:30 +0000321 sleep 100m
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000322 if !getchar(1)
323 break
324 endif
325 while getchar(1)
326 getchar()
327 endwhile
Bram Moolenaar83030352022-11-16 16:08:30 +0000328 endwhile
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000329 ch_logfile('', '')
Bram Moolenaar83030352022-11-16 16:08:30 +0000330
Bram Moolenaara44c7812022-11-15 22:59:07 +0000331 ch_logfile('keylog', 'w')
332 echo $'Press the {entry[0]} key (q to quit):'
333 var r = getcharstr()
334 ch_logfile('', '')
335 if r == 'q'
336 break
337 endif
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000338
Bram Moolenaar83030352022-11-16 16:08:30 +0000339 log = readfile('keylog')
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000340 delete('keylog')
Bram Moolenaara44c7812022-11-15 22:59:07 +0000341 if len(log) < 2
342 echoerr 'failed to read result'
343 return
344 endif
345 var done = false
346 for line in log
347 if line =~ 'raw key input'
348 var code = substitute(line, '.*raw key input: "\([^"]*\).*', '\1', '')
349
Bram Moolenaar83030352022-11-16 16:08:30 +0000350 # Remove any version termresponse
351 code = substitute(code, version_pattern, '', 'g')
352
353 # Remove any XTGETTCAP replies.
354 const cappat = "\<Esc>P[01]+\\k\\+=\\x*\<Esc>\\\\"
355 code = substitute(code, cappat, '', 'g')
356
357 # Remove any kitty status reply
Bram Moolenaar236dffa2022-11-18 21:20:25 +0000358 code = substitute(code, kitty_status_pattern, '', 'g')
Bram Moolenaar83030352022-11-16 16:08:30 +0000359 if code == ''
360 continue
361 endif
362
363 # Convert the literal bytes into hex. If 'x' was pressed then clear
364 # the entry.
Bram Moolenaara44c7812022-11-15 22:59:07 +0000365 var hex = ''
Bram Moolenaar83030352022-11-16 16:08:30 +0000366 if code != 'x'
367 hex = Literal2hex(code)
368 endif
369
Bram Moolenaara44c7812022-11-15 22:59:07 +0000370 keycodes[name][entry[1]] = hex
371 done = true
372 break
373 endif
374 endfor
375 if !done
376 echo 'Code not found in log'
377 endif
378 endfor
379enddef
380
381# Action: Add key codes for a new terminal.
382def ActionAdd()
383 var name = input('Enter name of the terminal: ')
384 echo "\n"
385 if index(keys(keycodes), name) >= 0
386 echoerr $'Terminal {name} already exists'
387 return
388 endif
389
390 DoTerm(name)
391enddef
392
393# Action: Replace key codes for an already known terminal.
394def ActionReplace()
395 var terms = keys(keycodes)
396 if len(terms) == 0
397 echo 'No terminal results yet'
398 return
399 endif
400
401 var choice = inputlist(['Select:'] + terms->copy()->map((idx, arg) => (idx + 1) .. ': ' .. arg))
402 echo "\n"
403 if choice > 0 && choice <= len(terms)
404 DoTerm(terms[choice - 1])
Bram Moolenaar83030352022-11-16 16:08:30 +0000405 else
406 echo 'invalid index'
Bram Moolenaara44c7812022-11-15 22:59:07 +0000407 endif
Bram Moolenaar83030352022-11-16 16:08:30 +0000408enddef
409
410# Action: Clear key codes for an already known terminal.
411def ActionClear()
412 var terms = keys(keycodes)
413 if len(terms) == 0
414 echo 'No terminal results yet'
415 return
416 endif
417
418 var choice = inputlist(['Select:'] + terms->copy()->map((idx, arg) => (idx + 1) .. ': ' .. arg))
419 echo "\n"
420 if choice > 0 && choice <= len(terms)
421 remove(keycodes, terms[choice - 1])
422 else
423 echo 'invalid index'
424 endif
Bram Moolenaara44c7812022-11-15 22:59:07 +0000425enddef
426
427# Action: Quit, possibly after saving the results first.
428def ActionQuit()
429 # If nothing was changed just quit
430 if keycodes == orig_keycodes
431 quit
432 endif
433
434 while true
435 var res = input("Save the changed key codes (y/n)? ")
436 if res == 'n'
437 quit
438 endif
439 if res == 'y'
440 WriteKeycodes()
441 quit
442 endif
443 echo 'invalid reply'
444 endwhile
445enddef
446
447# The main loop
448while true
449 var action = inputlist(['Select operation:',
450 '1. List results',
451 '2. Add results for a new terminal',
452 '3. Replace results',
Bram Moolenaar83030352022-11-16 16:08:30 +0000453 '4. Clear results',
454 '5. Quit',
Bram Moolenaara44c7812022-11-15 22:59:07 +0000455 ])
456 echo "\n"
457 if action == 1
458 ActionList()
459 elseif action == 2
460 ActionAdd()
461 elseif action == 3
462 ActionReplace()
463 elseif action == 4
Bram Moolenaar83030352022-11-16 16:08:30 +0000464 ActionClear()
465 elseif action == 5
Bram Moolenaara44c7812022-11-15 22:59:07 +0000466 ActionQuit()
467 endif
468endwhile