[console] Allow for named keyboard mappings

Separate the concept of a keyboard mapping from a list of remapped
keys, to allow for the possibility of supporting multiple keyboard
mappings at runtime.

Signed-off-by: Michael Brown <mcb30@ipxe.org>
This commit is contained in:
Michael Brown
2022-02-14 13:22:48 +00:00
parent 1150321595
commit 871dd236d4
33 changed files with 373 additions and 130 deletions

View File

@@ -79,7 +79,7 @@ class KeyModifiers(Flag):
return 3 + bin(self.value).count('1')
@dataclass
@dataclass(frozen=True)
class Key:
"""A single key definition"""
@@ -120,8 +120,8 @@ class Key:
return None
class KeyMapping(UserDict[KeyModifiers, Sequence[Key]]):
"""A keyboard mapping"""
class KeyLayout(UserDict[KeyModifiers, Sequence[Key]]):
"""A keyboard layout"""
BKEYMAP_MAGIC: ClassVar[bytes] = b'bkeymap'
"""Magic signature for output produced by 'loadkeys -b'"""
@@ -163,16 +163,16 @@ class KeyMapping(UserDict[KeyModifiers, Sequence[Key]]):
@property
def unshifted(self):
"""Basic unshifted key mapping"""
"""Basic unshifted keyboard layout"""
return self[KeyModifiers.NONE]
@property
def shifted(self):
"""Basic shifted key mapping"""
"""Basic shifted keyboard layout"""
return self[KeyModifiers.SHIFT]
@classmethod
def load(cls, name: str) -> KeyMapping:
def load(cls, name: str) -> KeyLayout:
"""Load keymap using 'loadkeys -b'"""
bkeymap = subprocess.check_output(["loadkeys", "-u", "-b", name])
if not bkeymap.startswith(cls.BKEYMAP_MAGIC):
@@ -181,21 +181,21 @@ class KeyMapping(UserDict[KeyModifiers, Sequence[Key]]):
included = bkeymap[:cls.MAX_NR_KEYMAPS]
if len(included) != cls.MAX_NR_KEYMAPS:
raise ValueError("Invalid bkeymap inclusion list")
keymaps = bkeymap[cls.MAX_NR_KEYMAPS:]
bkeymap = bkeymap[cls.MAX_NR_KEYMAPS:]
keys = {}
for modifiers in map(KeyModifiers, range(cls.MAX_NR_KEYMAPS)):
if included[modifiers.value]:
fmt = Struct('<%dH' % cls.NR_KEYS)
keymap = keymaps[:fmt.size]
if len(keymap) != fmt.size:
bkeylist = bkeymap[:fmt.size]
if len(bkeylist) != fmt.size:
raise ValueError("Invalid bkeymap map %#x" %
modifiers.value)
keys[modifiers] = [
Key(modifiers=modifiers, keycode=keycode, keysym=keysym)
for keycode, keysym in enumerate(fmt.unpack(keymap))
for keycode, keysym in enumerate(fmt.unpack(bkeylist))
]
keymaps = keymaps[len(keymap):]
if keymaps:
bkeymap = bkeymap[len(bkeylist):]
if bkeymap:
raise ValueError("Trailing bkeymap data")
for modifiers, fixups in cls.FIXUPS.get(name, {}).items():
for keycode, keysym in fixups:
@@ -218,8 +218,8 @@ class KeyMapping(UserDict[KeyModifiers, Sequence[Key]]):
}
class BiosKeyMapping(KeyMapping):
"""Keyboard mapping as used by the BIOS
class BiosKeyLayout(KeyLayout):
"""Keyboard layout as used by the BIOS
To allow for remappings of the somewhat interesting key 86, we
arrange for our keyboard drivers to generate this key as "\\|"
@@ -247,22 +247,56 @@ class BiosKeyMapping(KeyMapping):
return inverse
class KeymapKeys(UserDict[str, str]):
"""An ASCII character remapping"""
@classmethod
def ascii_name(cls, char: str) -> str:
"""ASCII character name"""
if char == '\\':
name = "'\\\\'"
elif char == '\'':
name = "'\\\''"
elif ord(char) & BiosKeyLayout.KEY_PSEUDO:
name = "Pseudo-%s" % cls.ascii_name(
chr(ord(char) & ~BiosKeyLayout.KEY_PSEUDO)
)
elif char.isprintable():
name = "'%s'" % char
elif ord(char) <= 0x1a:
name = "Ctrl-%c" % (ord(char) + 0x40)
else:
name = "0x%02x" % ord(char)
return name
@property
def code(self):
"""Generated source code for C array"""
return '{\n' + ''.join(
'\t{ 0x%02x, 0x%02x },\t/* %s => %s */\n' % (
ord(source), ord(target),
self.ascii_name(source), self.ascii_name(target)
)
for source, target in self.items()
) + '\t{ 0, 0 }\n}'
@dataclass
class KeyRemapping:
"""A keyboard remapping"""
class Keymap:
"""An iPXE keyboard mapping"""
name: str
"""Mapping name"""
source: KeyMapping
"""Source keyboard mapping"""
source: KeyLayout
"""Source keyboard layout"""
target: KeyMapping
"""Target keyboard mapping"""
target: KeyLayout
"""Target keyboard layout"""
@property
def ascii(self) -> MutableMapping[str, str]:
"""Remapped ASCII key table"""
def basic(self) -> KeymapKeys:
"""Basic remapping table"""
# Construct raw mapping from source ASCII to target ASCII
raw = {source: self.target[key.modifiers][key.keycode].ascii
for source, key in self.source.inverse.items()}
@@ -273,7 +307,7 @@ class KeyRemapping:
if target
and ord(source) != 0x7f
and ord(target) != 0x7f
and ord(source) & ~BiosKeyMapping.KEY_PSEUDO != ord(target)}
and ord(source) & ~BiosKeyLayout.KEY_PSEUDO != ord(target)}
# Recursively delete any mappings that would produce
# unreachable alphanumerics (e.g. the "il" keymap, which maps
# away the whole lower-case alphabet)
@@ -291,35 +325,17 @@ class KeyRemapping:
if digits not in (shifted, unshifted):
raise ValueError("Inconsistent numeric remapping %s / %s" %
(unshifted, shifted))
return dict(sorted(table.items()))
return KeymapKeys(dict(sorted(table.items())))
@property
def cname(self) -> str:
def cname(self, suffix: str) -> str:
"""C variable name"""
return re.sub(r'\W', '_', self.name) + "_mapping"
@classmethod
def ascii_name(cls, char: str) -> str:
"""ASCII character name"""
if char == '\\':
name = "'\\\\'"
elif char == '\'':
name = "'\\\''"
elif ord(char) & BiosKeyMapping.KEY_PSEUDO:
name = "Pseudo-%s" % cls.ascii_name(
chr(ord(char) & ~BiosKeyMapping.KEY_PSEUDO)
)
elif char.isprintable():
name = "'%s'" % char
elif ord(char) <= 0x1a:
name = "Ctrl-%c" % (ord(char) + 0x40)
else:
name = "0x%02x" % ord(char)
return name
return re.sub(r'\W', '_', (self.name + '_' + suffix))
@property
def code(self) -> str:
"""Generated source code"""
keymap_name = self.cname("keymap")
basic_name = self.cname("basic")
code = textwrap.dedent(f"""
/** @file
*
@@ -333,17 +349,15 @@ class KeyRemapping:
#include <ipxe/keymap.h>
/** "{self.name}" keyboard mapping */
struct key_mapping {self.cname}[] __keymap = {{
""").lstrip() + ''.join(
'\t{ 0x%02x, 0x%02x },\t/* %s => %s */\n' % (
ord(source), ord(target),
self.ascii_name(source), self.ascii_name(target)
)
for source, target in self.ascii.items()
) + textwrap.dedent("""
};
""").strip()
/** "{self.name}" basic remapping */
static struct keymap_key {basic_name}[] = %s;
/** "{self.name}" keyboard map */
struct keymap {keymap_name} __keymap = {{
\t.name = "{self.name}",
\t.basic = {basic_name},
}};
""").strip() % self.basic.code
return code
@@ -356,12 +370,12 @@ if __name__ == '__main__':
parser.add_argument('layout', help="Target keyboard layout")
args = parser.parse_args()
# Load source and target keymaps
source = BiosKeyMapping.load('us')
target = KeyMapping.load(args.layout)
# Load source and target keyboard layouts
source = BiosKeyLayout.load('us')
target = KeyLayout.load(args.layout)
# Construct remapping
remap = KeyRemapping(name=args.layout, source=source, target=target)
# Construct keyboard mapping
keymap = Keymap(name=args.layout, source=source, target=target)
# Output generated code
print(remap.code)
print(keymap.code)