blob: fe854d1b3d38ac635b045ad56ecc73322c431d63 [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 Moolenaar83030352022-11-16 16:08:30 +0000137 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 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 Moolenaar83030352022-11-16 16:08:30 +0000201 # Enable modifyOtherKeys level 2 - no status is reported
Bram Moolenaara44c7812022-11-15 22:59:07 +0000202 &t_TI = "\<Esc>[>4;2m"
203 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
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 Moolenaara44c7812022-11-15 22:59:07 +0000226 # executing a dummy shell command will output t_TI
227 !echo >/dev/null
228
Bram Moolenaar83030352022-11-16 16:08:30 +0000229 # 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 Moolenaara44c7812022-11-15 22:59:07 +0000254 if !has_key(keycodes, name)
255 keycodes[name] = {}
256 endif
257 keycodes[name]['protocol'] = proto_name
Bram Moolenaar83030352022-11-16 16:08:30 +0000258 keycodes[name]['version'] = ''
259 keycodes[name]['status'] = ''
Bram Moolenaara44c7812022-11-15 22:59:07 +0000260
Bram Moolenaar83030352022-11-16 16:08:30 +0000261 # 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 Moolenaara44c7812022-11-15 22:59:07 +0000284
285 for entry in key_entries
Bram Moolenaar83030352022-11-16 16:08:30 +0000286 # 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 Moolenaara44c7812022-11-15 22:59:07 +0000293 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 Moolenaar83030352022-11-16 16:08:30 +0000300 log = readfile('keylog')
301 if entry[1] == 'Tab'
302# keep a copy
303rename('keylog', 'keylog-tab')
304 else
305 delete('keylog')
306 endif
Bram Moolenaara44c7812022-11-15 22:59:07 +0000307 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 Moolenaar83030352022-11-16 16:08:30 +0000316 # 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 Moolenaara44c7812022-11-15 22:59:07 +0000331 var hex = ''
Bram Moolenaar83030352022-11-16 16:08:30 +0000332 if code != 'x'
333 hex = Literal2hex(code)
334 endif
335
Bram Moolenaara44c7812022-11-15 22:59:07 +0000336 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
345enddef
346
347# Action: Add key codes for a new terminal.
348def 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)
357enddef
358
359# Action: Replace key codes for an already known terminal.
360def 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 Moolenaar83030352022-11-16 16:08:30 +0000371 else
372 echo 'invalid index'
Bram Moolenaara44c7812022-11-15 22:59:07 +0000373 endif
Bram Moolenaar83030352022-11-16 16:08:30 +0000374enddef
375
376# Action: Clear key codes for an already known terminal.
377def 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 Moolenaara44c7812022-11-15 22:59:07 +0000391enddef
392
393# Action: Quit, possibly after saving the results first.
394def 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
411enddef
412
413# The main loop
414while true
415 var action = inputlist(['Select operation:',
416 '1. List results',
417 '2. Add results for a new terminal',
418 '3. Replace results',
Bram Moolenaar83030352022-11-16 16:08:30 +0000419 '4. Clear results',
420 '5. Quit',
Bram Moolenaara44c7812022-11-15 22:59:07 +0000421 ])
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 Moolenaar83030352022-11-16 16:08:30 +0000430 ActionClear()
431 elseif action == 5
Bram Moolenaara44c7812022-11-15 22:59:07 +0000432 ActionQuit()
433 endif
434endwhile