[intel] Add PCI ID for I219-V and -LM 16,17
[ipxe.git] / src / util / genkeymap.py
1 #!/usr/bin/env python3
2 #
3 # Copyright (C) 2022 Michael Brown <mbrown@fensystems.co.uk>.
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License as
7 # published by the Free Software Foundation; either version 2 of the
8 # License, or any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 # 02110-1301, USA.
19
20 """Generate iPXE keymaps"""
21
22 from __future__ import annotations
23
24 import argparse
25 from collections import UserDict
26 from collections.abc import Sequence, Mapping, MutableMapping
27 from dataclasses import dataclass
28 from enum import Flag, IntEnum
29 import re
30 import subprocess
31 from struct import Struct
32 import textwrap
33 from typing import ClassVar, Optional
34
35
36 BACKSPACE = chr(0x7f)
37 """Backspace character"""
38
39
40 class KeyType(IntEnum):
41 """Key types"""
42
43 LATIN = 0
44 FN = 1
45 SPEC = 2
46 PAD = 3
47 DEAD = 4
48 CONS = 5
49 CUR = 6
50 SHIFT = 7
51 META = 8
52 ASCII = 9
53 LOCK = 10
54 LETTER = 11
55 SLOCK = 12
56 DEAD2 = 13
57 BRL = 14
58 UNKNOWN = 0xf0
59
60
61 class DeadKey(IntEnum):
62 """Dead keys"""
63
64 GRAVE = 0
65 CIRCUMFLEX = 2
66 TILDE = 3
67
68
69 class KeyModifiers(Flag):
70 """Key modifiers"""
71
72 NONE = 0
73 SHIFT = 1
74 ALTGR = 2
75 CTRL = 4
76 ALT = 8
77 SHIFTL = 16
78 SHIFTR = 32
79 CTRLL = 64
80 CTRLR = 128
81
82 @property
83 def complexity(self) -> int:
84 """Get complexity value of applied modifiers"""
85 if self == self.NONE:
86 return 0
87 if self == self.SHIFT:
88 return 1
89 if self == self.CTRL:
90 return 2
91 return 3 + bin(self.value).count('1')
92
93
94 @dataclass(frozen=True)
95 class Key:
96 """A single key definition"""
97
98 keycode: int
99 """Opaque keycode"""
100
101 keysym: int
102 """Key symbol"""
103
104 modifiers: KeyModifiers
105 """Applied modifiers"""
106
107 ASCII_TYPES: ClassVar[set[KeyType]] = {KeyType.LATIN, KeyType.ASCII,
108 KeyType.LETTER}
109 """Key types with direct ASCII values"""
110
111 DEAD_KEYS: ClassVar[Mapping[int, str]] = {
112 DeadKey.GRAVE: '`',
113 DeadKey.CIRCUMFLEX: '^',
114 DeadKey.TILDE: '~',
115 }
116 """Dead key replacement ASCII values"""
117
118 @property
119 def keytype(self) -> Optional[KeyType]:
120 """Key type"""
121 try:
122 return KeyType(self.keysym >> 8)
123 except ValueError:
124 return None
125
126 @property
127 def value(self) -> int:
128 """Key value"""
129 return self.keysym & 0xff
130
131 @property
132 def ascii(self) -> Optional[str]:
133 """ASCII character"""
134 keytype = self.keytype
135 value = self.value
136 if keytype in self.ASCII_TYPES:
137 char = chr(value)
138 if value and char.isascii():
139 return char
140 if keytype == KeyType.DEAD:
141 return self.DEAD_KEYS.get(value)
142 return None
143
144
145 class KeyLayout(UserDict[KeyModifiers, Sequence[Key]]):
146 """A keyboard layout"""
147
148 BKEYMAP_MAGIC: ClassVar[bytes] = b'bkeymap'
149 """Magic signature for output produced by 'loadkeys -b'"""
150
151 MAX_NR_KEYMAPS: ClassVar[int] = 256
152 """Maximum number of keymaps produced by 'loadkeys -b'"""
153
154 NR_KEYS: ClassVar[int] = 128
155 """Number of keys in each keymap produced by 'loadkeys -b'"""
156
157 KEY_BACKSPACE: ClassVar[int] = 14
158 """Key code for backspace
159
160 Keyboard maps seem to somewhat arbitrarily pick an interpretation
161 for the backspace key and its various modifiers, according to the
162 personal preference of the keyboard map transcriber.
163 """
164
165 KEY_NON_US: ClassVar[int] = 86
166 """Key code 86
167
168 Key code 86 is somewhat bizarre. It doesn't physically exist on
169 most US keyboards. The database used by "loadkeys" defines it as
170 "<>", while most other databases either define it as a duplicate
171 "\\|" or omit it entirely.
172 """
173
174 FIXUPS: ClassVar[Mapping[str, Mapping[KeyModifiers,
175 Sequence[tuple[int, int]]]]] = {
176 'us': {
177 # Redefine erroneous key 86 as generating "\\|"
178 KeyModifiers.NONE: [(KEY_NON_US, ord('\\'))],
179 KeyModifiers.SHIFT: [(KEY_NON_US, ord('|'))],
180 # Treat Ctrl-Backspace as producing Backspace rather than Ctrl-H
181 KeyModifiers.CTRL: [(KEY_BACKSPACE, ord(BACKSPACE))],
182 },
183 'il': {
184 # Redefine some otherwise unreachable ASCII characters
185 # using the closest available approximation
186 KeyModifiers.ALTGR: [(0x28, ord('\'')), (0x2b, ord('`')),
187 (0x35, ord('/'))],
188 },
189 'mt': {
190 # Redefine erroneous key 86 as generating "\\|"
191 KeyModifiers.NONE: [(KEY_NON_US, ord('\\'))],
192 KeyModifiers.SHIFT: [(KEY_NON_US, ord('|'))],
193 },
194 }
195 """Fixups for erroneous keymappings produced by 'loadkeys -b'"""
196
197 @property
198 def unshifted(self):
199 """Basic unshifted keyboard layout"""
200 return self[KeyModifiers.NONE]
201
202 @property
203 def shifted(self):
204 """Basic shifted keyboard layout"""
205 return self[KeyModifiers.SHIFT]
206
207 @classmethod
208 def load(cls, name: str) -> KeyLayout:
209 """Load keymap using 'loadkeys -b'"""
210 bkeymap = subprocess.check_output(["loadkeys", "-u", "-b", name])
211 if not bkeymap.startswith(cls.BKEYMAP_MAGIC):
212 raise ValueError("Invalid bkeymap magic signature")
213 bkeymap = bkeymap[len(cls.BKEYMAP_MAGIC):]
214 included = bkeymap[:cls.MAX_NR_KEYMAPS]
215 if len(included) != cls.MAX_NR_KEYMAPS:
216 raise ValueError("Invalid bkeymap inclusion list")
217 bkeymap = bkeymap[cls.MAX_NR_KEYMAPS:]
218 keys = {}
219 for modifiers in map(KeyModifiers, range(cls.MAX_NR_KEYMAPS)):
220 if included[modifiers.value]:
221 fmt = Struct('<%dH' % cls.NR_KEYS)
222 bkeylist = bkeymap[:fmt.size]
223 if len(bkeylist) != fmt.size:
224 raise ValueError("Invalid bkeymap map %#x" %
225 modifiers.value)
226 keys[modifiers] = [
227 Key(modifiers=modifiers, keycode=keycode, keysym=keysym)
228 for keycode, keysym in enumerate(fmt.unpack(bkeylist))
229 ]
230 bkeymap = bkeymap[len(bkeylist):]
231 if bkeymap:
232 raise ValueError("Trailing bkeymap data")
233 for modifiers, fixups in cls.FIXUPS.get(name, {}).items():
234 for keycode, keysym in fixups:
235 keys[modifiers][keycode] = Key(modifiers=modifiers,
236 keycode=keycode, keysym=keysym)
237 return cls(keys)
238
239 @property
240 def inverse(self) -> MutableMapping[str, Key]:
241 """Construct inverse mapping from ASCII value to key"""
242 return {
243 key.ascii: key
244 # Give priority to simplest modifier for a given ASCII code
245 for modifiers in sorted(self.keys(), reverse=True,
246 key=lambda x: (x.complexity, x.value))
247 # Give priority to lowest keycode for a given ASCII code
248 for key in reversed(self[modifiers])
249 # Ignore keys with no ASCII value
250 if key.ascii
251 }
252
253
254 class BiosKeyLayout(KeyLayout):
255 """Keyboard layout as used by the BIOS
256
257 To allow for remappings of the somewhat interesting key 86, we
258 arrange for our keyboard drivers to generate this key as "\\|"
259 with the high bit set.
260 """
261
262 KEY_PSEUDO: ClassVar[int] = 0x80
263 """Flag used to indicate a fake ASCII value"""
264
265 KEY_NON_US_UNSHIFTED: ClassVar[str] = chr(KEY_PSEUDO | ord('\\'))
266 """Fake ASCII value generated for unshifted key code 86"""
267
268 KEY_NON_US_SHIFTED: ClassVar[str] = chr(KEY_PSEUDO | ord('|'))
269 """Fake ASCII value generated for shifted key code 86"""
270
271 @property
272 def inverse(self) -> MutableMapping[str, Key]:
273 inverse = super().inverse
274 assert len(inverse) == 0x7f
275 inverse[self.KEY_NON_US_UNSHIFTED] = self.unshifted[self.KEY_NON_US]
276 inverse[self.KEY_NON_US_SHIFTED] = self.shifted[self.KEY_NON_US]
277 assert all(x.modifiers in {KeyModifiers.NONE, KeyModifiers.SHIFT,
278 KeyModifiers.CTRL}
279 for x in inverse.values())
280 return inverse
281
282
283 class KeymapKeys(UserDict[str, Optional[str]]):
284 """An ASCII character remapping"""
285
286 @classmethod
287 def ascii_name(cls, char: str) -> str:
288 """ASCII character name"""
289 if char == '\\':
290 name = "'\\\\'"
291 elif char == '\'':
292 name = "'\\\''"
293 elif ord(char) & BiosKeyLayout.KEY_PSEUDO:
294 name = "Pseudo-%s" % cls.ascii_name(
295 chr(ord(char) & ~BiosKeyLayout.KEY_PSEUDO)
296 )
297 elif char.isprintable():
298 name = "'%s'" % char
299 elif ord(char) <= 0x1a:
300 name = "Ctrl-%c" % (ord(char) + 0x40)
301 else:
302 name = "0x%02x" % ord(char)
303 return name
304
305 @property
306 def code(self):
307 """Generated source code for C array"""
308 return '{\n' + ''.join(
309 '\t{ 0x%02x, 0x%02x },\t/* %s => %s */\n' % (
310 ord(source), ord(target),
311 self.ascii_name(source), self.ascii_name(target)
312 )
313 for source, target in self.items()
314 if target
315 and ord(source) & ~BiosKeyLayout.KEY_PSEUDO != ord(target)
316 ) + '\t{ 0, 0 }\n}'
317
318
319 @dataclass
320 class Keymap:
321 """An iPXE keyboard mapping"""
322
323 name: str
324 """Mapping name"""
325
326 source: KeyLayout
327 """Source keyboard layout"""
328
329 target: KeyLayout
330 """Target keyboard layout"""
331
332 @property
333 def basic(self) -> KeymapKeys:
334 """Basic remapping table"""
335 # Construct raw mapping from source ASCII to target ASCII
336 raw = {source: self.target[key.modifiers][key.keycode].ascii
337 for source, key in self.source.inverse.items()}
338 # Eliminate any identity mappings, or mappings that attempt to
339 # remap the backspace key
340 table = {source: target for source, target in raw.items()
341 if source != target
342 and source != BACKSPACE
343 and target != BACKSPACE}
344 # Recursively delete any mappings that would produce
345 # unreachable alphanumerics (e.g. the "il" keymap, which maps
346 # away the whole lower-case alphabet)
347 while True:
348 unreachable = set(table.keys()) - set(table.values())
349 delete = {x for x in unreachable if x.isascii() and x.isalnum()}
350 if not delete:
351 break
352 table = {k: v for k, v in table.items() if k not in delete}
353 # Sanity check: ensure that all numerics are reachable using
354 # the same shift state
355 digits = '1234567890'
356 unshifted = ''.join(table.get(x) or x for x in '1234567890')
357 shifted = ''.join(table.get(x) or x for x in '!@#$%^&*()')
358 if digits not in (shifted, unshifted):
359 raise ValueError("Inconsistent numeric remapping %s / %s" %
360 (unshifted, shifted))
361 return KeymapKeys(dict(sorted(table.items())))
362
363 @property
364 def altgr(self) -> KeymapKeys:
365 """AltGr remapping table"""
366 # Construct raw mapping from source ASCII to target ASCII
367 raw = {
368 source: next((self.target[x][key.keycode].ascii
369 for x in (key.modifiers | KeyModifiers.ALTGR,
370 KeyModifiers.ALTGR, key.modifiers)
371 if x in self.target
372 and self.target[x][key.keycode].ascii), None)
373 for source, key in self.source.inverse.items()
374 }
375 # Identify printable keys that are unreachable via the basic map
376 basic = self.basic
377 unmapped = set(x for x in basic.keys()
378 if x.isascii() and x.isprintable())
379 remapped = set(basic.values())
380 unreachable = unmapped - remapped
381 # Eliminate any mappings for unprintable characters, or
382 # mappings for characters that are reachable via the basic map
383 table = {source: target for source, target in raw.items()
384 if source.isprintable()
385 and target in unreachable}
386 # Check that all characters are now reachable
387 unreachable -= set(table.values())
388 if unreachable:
389 raise ValueError("Unreachable characters: %s" % ', '.join(
390 KeymapKeys.ascii_name(x) for x in sorted(unreachable)
391 ))
392 return KeymapKeys(dict(sorted(table.items())))
393
394 def cname(self, suffix: str) -> str:
395 """C variable name"""
396 return re.sub(r'\W', '_', (self.name + '_' + suffix))
397
398 @property
399 def code(self) -> str:
400 """Generated source code"""
401 keymap_name = self.cname("keymap")
402 basic_name = self.cname("basic")
403 altgr_name = self.cname("altgr")
404 attribute = "__keymap_default" if self.name == "us" else "__keymap"
405 code = textwrap.dedent(f"""
406 /** @file
407 *
408 * "{self.name}" keyboard mapping
409 *
410 * This file is automatically generated; do not edit
411 *
412 */
413
414 FILE_LICENCE ( PUBLIC_DOMAIN );
415
416 #include <ipxe/keymap.h>
417
418 /** "{self.name}" basic remapping */
419 static struct keymap_key {basic_name}[] = %s;
420
421 /** "{self.name}" AltGr remapping */
422 static struct keymap_key {altgr_name}[] = %s;
423
424 /** "{self.name}" keyboard map */
425 struct keymap {keymap_name} {attribute} = {{
426 \t.name = "{self.name}",
427 \t.basic = {basic_name},
428 \t.altgr = {altgr_name},
429 }};
430 """).strip() % (self.basic.code, self.altgr.code)
431 return code
432
433
434 if __name__ == '__main__':
435
436 # Parse command-line arguments
437 parser = argparse.ArgumentParser(description="Generate iPXE keymaps")
438 parser.add_argument('--verbose', '-v', action='count', default=0,
439 help="Increase verbosity")
440 parser.add_argument('layout', help="Target keyboard layout")
441 args = parser.parse_args()
442
443 # Load source and target keyboard layouts
444 source = BiosKeyLayout.load('us')
445 target = KeyLayout.load(args.layout)
446
447 # Construct keyboard mapping
448 keymap = Keymap(name=args.layout, source=source, target=target)
449
450 # Output generated code
451 print(keymap.code)