From 6e5ed09a6146c85d770448a93132e6fb94982108 Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Fri, 5 Apr 2024 21:45:45 +0200 Subject: [PATCH 01/17] Typematrix --- kalamine/data/geometry.yaml | 35 ++++++++++++++++++++++++++++++++ kalamine/data/scan_codes.yaml | 6 ++++++ kalamine/generators/ahk.py | 12 +++++++---- kalamine/generators/keylayout.py | 2 ++ kalamine/generators/klc.py | 4 ++++ kalamine/generators/web.py | 4 ++++ kalamine/generators/xkb.py | 26 +++++++++++++++++++++--- kalamine/layout.py | 11 ++++++++++ kalamine/utils.py | 24 +++++++++++++++++++++- 9 files changed, 116 insertions(+), 8 deletions(-) diff --git a/kalamine/data/geometry.yaml b/kalamine/data/geometry.yaml index 3045e25..ca7df46 100644 --- a/kalamine/data/geometry.yaml +++ b/kalamine/data/geometry.yaml @@ -162,3 +162,38 @@ ERGO: keys: [ ac01, ac02, ac03, ac04, ac05, ac06, ac07, ac08, ac09, ac10, ac11, bksl ] - offset: 2 keys: [ lsgt, ab01, ab02, ab03, ab04, ab05, ab06, ab07, ab08, ab09, ab10 ] + +TYPEMATRIX: + template: | + ╭╌╌╌╌╌┰─────┬─────┬─────┬─────┬─────┰─────┬─────┬─────┬─────┬─────┰╌╌╌╌╌┬╌╌╌╌╌╮ + ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ + ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ + ├╌╌╌╌╌╂─────┼─────┼─────┼─────┼─────╂─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┼╌╌╌╌╌┤ + ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ + ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ + ├╌╌╌╌╌╂─────┼─────┼─────┼─────┼─────╂─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┼╌╌╌╌╌┤ + ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ + ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ + ┆ ┠─────┼─────┼─────┼─────┼─────╂─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┤ ┆ + ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ + ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ + ├╌╌╌╌╌╂─────┼─────┼─────┼─────┴─────┸─────┴─────┼─────┼─────┴─────┸╌╌╌╌╌┴╌╌╌╌╌╯ + ┆ ┆ ┆ ┆ ┆ ┆ ┆ + ┆ ┆ ┆ ┆ ┆ ┆ ┆ + ╰╌╌╌╌╌┴╌╌╌╌╌┴╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┤ + ┆ ┆ ┆ ┆ + ┆ ┆ ┆ ┆ + ╰╌╌╌╌╌╯ ╰╌╌╌╌╌╯ + rows: + - offset: 2 + keys: [ tlde, ae01, ae02, ae03, ae04, ae05, ae06, ae07, ae08, ae09, ae10, ae11, ae12 ] + - offset: 2 + keys: [ tab, ad01, ad02, ad03, ad04, ad05, ad06, ad07, ad08, ad09, ad10, ad11, ad12 ] + - offset: 2 + keys: [ lfsh, ac01, ac02, ac03, ac04, ac05, ac06, ac07, ac08, ac09, ac10, ac11 ] + - offset: 2 + keys: [ lfsh, ab01, ab02, ab03, ab04, ab05, ab06, ab07, ab08, ab09, ab10, bksl ] + - offset: 2 + keys: [ lctl, lsgt, menu, muhe, spce, spce, spce, spce, henk ] + - offset: 20 + keys: [ lalt, xxxx, xxxx, xxxx, xxxx, ralt ] diff --git a/kalamine/data/scan_codes.yaml b/kalamine/data/scan_codes.yaml index e3aefd2..57333f4 100644 --- a/kalamine/data/scan_codes.yaml +++ b/kalamine/data/scan_codes.yaml @@ -186,3 +186,9 @@ web: ac11: 'Quote' ab11: 'IntlRo' lsgt: 'IntlBackslash' + + # Other + lalt: 'AltLeft' + ralt: 'AltRight' + muhe: 'NonConvert' + henk: 'Convert' diff --git a/kalamine/generators/ahk.py b/kalamine/generators/ahk.py index 47558b5..2d5f433 100644 --- a/kalamine/generators/ahk.py +++ b/kalamine/generators/ahk.py @@ -44,8 +44,10 @@ def ahk_actions(symbol: str) -> Dict[str, str]: output.append("") continue - if key_name in ["ae13", "ab11"]: # ABNT / JIS keys - continue # these two keys are not supported yet + # if key_name in ["ae13", "ab11"]: # ABNT / JIS keys + # continue # these two keys are not supported yet + if key_name not in SCAN_CODES['klc']: + continue sc = f"SC{SCAN_CODES['klc'][key_name]}" for i in ( @@ -89,8 +91,10 @@ def ahk_shortcuts(layout: "KeyboardLayout") -> List[str]: output.append("") continue - if key_name in ["ae13", "ab11"]: # ABNT / JIS keys - continue # these two keys are not supported yet + # if key_name in ["ae13", "ab11"]: # ABNT / JIS keys + # continue # these two keys are not supported yet + if key_name not in SCAN_CODES['klc']: + continue scan_code = SCAN_CODES["klc"][key_name] for i in [Layer.BASE, Layer.SHIFT]: diff --git a/kalamine/generators/keylayout.py b/kalamine/generators/keylayout.py index f084c65..501f962 100644 --- a/kalamine/generators/keylayout.py +++ b/kalamine/generators/keylayout.py @@ -47,6 +47,8 @@ def has_dead_keys(letter: str) -> bool: for key_name in LAYER_KEYS: if key_name in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet + if key_name not in SCAN_CODES['osx']: + continue if key_name.startswith("-"): if output: diff --git a/kalamine/generators/klc.py b/kalamine/generators/klc.py index bbc9375..329157c 100644 --- a/kalamine/generators/klc.py +++ b/kalamine/generators/klc.py @@ -107,6 +107,8 @@ def klc_keymap(layout: "KeyboardLayout") -> List[str]: if key_name in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet + if key_name not in SCAN_CODES['klc']: + continue symbols = [] description = "//" @@ -235,6 +237,8 @@ def c_keymap(layout: "KeyboardLayout") -> List[str]: if key_name in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet + if key_name not in SCAN_CODES['klc']: + continue symbols = [] dead_symbols = [] diff --git a/kalamine/generators/web.py b/kalamine/generators/web.py index 4c9d023..a65610b 100644 --- a/kalamine/generators/web.py +++ b/kalamine/generators/web.py @@ -24,6 +24,8 @@ def raw_json(layout: "KeyboardLayout") -> Dict: for key_name in LAYER_KEYS: if key_name.startswith("-"): continue + if key_name not in SCAN_CODES['web']: + continue chars = list("") for i in [Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT]: if key_name in layout.layers[i]: @@ -93,6 +95,8 @@ def same_symbol(key_name: str, lower: Layer, upper: Layer): for key_name in LAYER_KEYS: if key_name.startswith("-"): continue + if key_name not in SCAN_CODES['web']: + continue level = 0 for i in [ diff --git a/kalamine/generators/xkb.py b/kalamine/generators/xkb.py index 3ef688e..ce4cd39 100644 --- a/kalamine/generators/xkb.py +++ b/kalamine/generators/xkb.py @@ -10,9 +10,18 @@ from ..layout import KeyboardLayout from ..template import load_tpl, substitute_lines -from ..utils import DK_INDEX, LAYER_KEYS, ODK_ID, hex_ord, load_data +from ..utils import DK_INDEX, LAYER_KEYS, ODK_ID, SpecialSymbol, hex_ord, load_data XKB_KEY_SYM = load_data("key_sym") +XKB_SPECIAL_KEYSYMS = { + SpecialSymbol.Alt.value: "Alt_L", + SpecialSymbol.AltGr.value: "ISO_Level3_Shift", + SpecialSymbol.CapsLock.value: "Caps_Lock", + SpecialSymbol.Compose.value: "Multi_key", + SpecialSymbol.Control.value: "Control_L", + SpecialSymbol.Shift.value: "Shift_L", +} +XKB_KEY_SYM.update(XKB_SPECIAL_KEYSYMS) def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False) -> List[str]: @@ -57,9 +66,18 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False) -> List[str]: descs.append(desc) symbols.append(symbol.ljust(max_length)) + key_type = "" key = "{{[ {0}, {1}, {2}, {3}]}}" # 4-level layout by default description = "{0} {1} {2} {3}" - if layout.has_altgr and layout.has_1dk: + if all(s.startswith("VoidSymbol") for s in symbols): + continue + elif not symbols[0].startswith("VoidSymbol") and all(s == symbols[0] for s in symbols): + key = "{{{type}[{0}]}}" + description = "{0}" + key_type = "ONE_LEVEL" + symbols = [symbols[0]] + descs = [descs[0]] + elif layout.has_altgr and layout.has_1dk: # 6 layers are needed: they won't fit on the 4-level format. if xkbcomp: # user-space XKB keymap file (standalone) # standalone XKB files work best with a dual-group solution: @@ -75,7 +93,9 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False) -> List[str]: del descs[3] del descs[2] - line = f"key <{key_name.upper()}> {key.format(*symbols)};" + if key_type: + key_type = f"""type[Group1] = "{key_type}", """ + line = f"key <{key_name.upper()}> {key.format(*symbols, type=key_type)};" if show_description: line += (" // " + description.format(*descs)).rstrip() if line.endswith("\\"): diff --git a/kalamine/layout.py b/kalamine/layout.py index bae0295..9234808 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -13,6 +13,7 @@ LAYER_KEYS, ODK_ID, Layer, + SpecialSymbol, load_data, text_to_lines, upper_key, @@ -190,6 +191,16 @@ def __init__( text_to_lines(layout_data["altgr"]), rows, Layer.ALTGR ) + # Fill special symbols + special_symbols = frozenset(s.value for s in SpecialSymbol) + for key in LAYER_KEYS: + if base_symbol := self.layers[Layer.BASE].get(key): + if base_symbol not in special_symbols: + continue + for keys in self.layers.values(): + if key not in keys: + keys[key] = base_symbol + # space bar spc = SPACEBAR.copy() if "spacebar" in layout_data: diff --git a/kalamine/utils.py b/kalamine/utils.py index d19967e..dd06cce 100644 --- a/kalamine/utils.py +++ b/kalamine/utils.py @@ -1,6 +1,6 @@ import pkgutil from dataclasses import dataclass -from enum import IntEnum +from enum import Enum, IntEnum, unique from typing import Dict, List, Optional import yaml @@ -66,6 +66,10 @@ def upper_key(letter: Optional[str], blank_if_obvious: bool = True) -> str: if letter is None: return " " + + special_symbols = {s.value: s.value for s in SpecialSymbol} + if letter in special_symbols: + return special_symbols[letter] custom_alpha = { "\u00df": "\u1e9e", # ß ẞ @@ -109,6 +113,15 @@ class DeadKeyDescr: ODK_ID = "**" # must match the value in dead_keys.yaml +@unique +class SpecialSymbol(Enum): + Alt = "⎇" + AltGr = "⇮" + CapsLock = "⇬" + Compose = "⎄" + Control = "⎈" + Shift = "⇧" + LAYER_KEYS = [ "- Digits", "ae01", @@ -167,4 +180,13 @@ class DeadKeyDescr: "lsgt", "- Space bar", "spce", + "menu", + "lfsh", + "rtsh", + "lalt", + "ralt", + "lctl", + "rctl", + "muhe", + "henk", ] From 9ce2b02d2a9686432f3a91e2020b631febf61134 Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Sat, 6 Apr 2024 12:40:58 +0200 Subject: [PATCH 02/17] Extra mapping & special symbols --- kalamine/data/geometry.yaml | 6 +- kalamine/generators/web.py | 4 +- kalamine/generators/xkb.py | 19 ++++--- kalamine/layout.py | 107 ++++++++++++++++++++++-------------- kalamine/utils.py | 85 ++++++++++++++++++++++++---- 5 files changed, 156 insertions(+), 65 deletions(-) diff --git a/kalamine/data/geometry.yaml b/kalamine/data/geometry.yaml index ca7df46..ad9da63 100644 --- a/kalamine/data/geometry.yaml +++ b/kalamine/data/geometry.yaml @@ -190,10 +190,10 @@ TYPEMATRIX: - offset: 2 keys: [ tab, ad01, ad02, ad03, ad04, ad05, ad06, ad07, ad08, ad09, ad10, ad11, ad12 ] - offset: 2 - keys: [ lfsh, ac01, ac02, ac03, ac04, ac05, ac06, ac07, ac08, ac09, ac10, ac11 ] + keys: [ lfsh, ac01, ac02, ac03, ac04, ac05, ac06, ac07, ac08, ac09, ac10, ac11, rtsh ] - offset: 2 - keys: [ lfsh, ab01, ab02, ab03, ab04, ab05, ab06, ab07, ab08, ab09, ab10, bksl ] + keys: [ lfsh, ab01, ab02, ab03, ab04, ab05, ab06, ab07, ab08, ab09, ab10, bksl, rtsh ] - offset: 2 - keys: [ lctl, lsgt, menu, muhe, spce, spce, spce, spce, henk ] + keys: [ lctl, lsgt, menu, muhe, spce, xxxx, xxxx, spce, henk ] - offset: 20 keys: [ lalt, xxxx, xxxx, xxxx, xxxx, ralt ] diff --git a/kalamine/generators/web.py b/kalamine/generators/web.py index a65610b..83ec2df 100644 --- a/kalamine/generators/web.py +++ b/kalamine/generators/web.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from ..layout import KeyboardLayout -from ..utils import LAYER_KEYS, ODK_ID, SCAN_CODES, Layer, upper_key +from ..utils import LAYER_KEYS, ODK_ID, SCAN_CODES, Layer, pretty_upper_key def raw_json(layout: "KeyboardLayout") -> Dict: @@ -83,7 +83,7 @@ def same_symbol(key_name: str, lower: Layer, upper: Layer): low = layout.layers[lower] if key_name not in up or key_name not in low: return False - return up[key_name] == upper_key(low[key_name], blank_if_obvious=False) + return up[key_name] == pretty_upper_key(low[key_name], blank_if_obvious=False) # Parse the SVG template # res = pkgutil.get_data(__package__, "templates/x-keyboard.svg") diff --git a/kalamine/generators/xkb.py b/kalamine/generators/xkb.py index ce4cd39..efdd117 100644 --- a/kalamine/generators/xkb.py +++ b/kalamine/generators/xkb.py @@ -10,17 +10,22 @@ from ..layout import KeyboardLayout from ..template import load_tpl, substitute_lines -from ..utils import DK_INDEX, LAYER_KEYS, ODK_ID, SpecialSymbol, hex_ord, load_data +from ..utils import DK_INDEX, LAYER_KEYS, ODK_ID, SystemSymbol, hex_ord, load_data XKB_KEY_SYM = load_data("key_sym") XKB_SPECIAL_KEYSYMS = { - SpecialSymbol.Alt.value: "Alt_L", - SpecialSymbol.AltGr.value: "ISO_Level3_Shift", - SpecialSymbol.CapsLock.value: "Caps_Lock", - SpecialSymbol.Compose.value: "Multi_key", - SpecialSymbol.Control.value: "Control_L", - SpecialSymbol.Shift.value: "Shift_L", + SystemSymbol.Alt.value: "Alt_L", + SystemSymbol.AltGr.value: "ISO_Level3_Shift", + SystemSymbol.BackSpace.value: "BackSpace", + SystemSymbol.CapsLock.value: "Caps_Lock", + SystemSymbol.Compose.value: "Multi_key", + SystemSymbol.Control.value: "Control_L", + SystemSymbol.Escape.value: "Escape", + SystemSymbol.Return.value: "Return", + SystemSymbol.Shift.value: "Shift_L", } +assert all(s.value in XKB_SPECIAL_KEYSYMS for s in SystemSymbol), \ + tuple(s for s in SystemSymbol if s.value not in XKB_SPECIAL_KEYSYMS) XKB_KEY_SYM.update(XKB_SPECIAL_KEYSYMS) diff --git a/kalamine/layout.py b/kalamine/layout.py index 9234808..9d1c6ff 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -14,8 +14,10 @@ ODK_ID, Layer, SpecialSymbol, + SystemSymbol, load_data, text_to_lines, + pretty_upper_key, upper_key, ) @@ -191,21 +193,11 @@ def __init__( text_to_lines(layout_data["altgr"]), rows, Layer.ALTGR ) - # Fill special symbols - special_symbols = frozenset(s.value for s in SpecialSymbol) - for key in LAYER_KEYS: - if base_symbol := self.layers[Layer.BASE].get(key): - if base_symbol not in special_symbols: - continue - for keys in self.layers.values(): - if key not in keys: - keys[key] = base_symbol - # space bar spc = SPACEBAR.copy() if "spacebar" in layout_data: for k in layout_data["spacebar"]: - spc[k] = layout_data["spacebar"][k] + spc[k] = self._parse_value(layout_data["spacebar"][k]) self.layers[Layer.BASE]["spce"] = " " self.layers[Layer.SHIFT]["spce"] = spc["shift"] if True or self.has_1dk: # XXX self.has_1dk is not defined yet @@ -217,8 +209,50 @@ def __init__( self.layers[Layer.ALTGR]["spce"] = spc["altgr"] self.layers[Layer.ALTGR_SHIFT]["spce"] = spc["altgr_shift"] + # Extra mapping + if mapping := layout_data.get("mapping"): + self._parse_mapping(mapping) + + # Fill special symbols + special_symbols = frozenset(s.value for s in SystemSymbol) + for key in LAYER_KEYS: + if base_symbol := self.layers[Layer.BASE].get(key): + if base_symbol not in special_symbols: + continue + for keys in self.layers.values(): + if key not in keys: + keys[key] = base_symbol + self._parse_dead_keys(spc) + def _parse_value(self, raw: str, strip=False) -> str: + return SpecialSymbol.parse(raw.strip() if strip else raw) + + def _parse_mapping(self, mapping: Dict[str, str | Dict[str, str]]): + layer: Layer | None + for raw_key, levels in mapping.items(): + # TODO: parse key in various ways (XKB, Linux keycode) + if raw_key not in LAYER_KEYS: + raise ValueError(f"Unknown key: “{raw_key}”") + key = raw_key + # Check for key clone + if isinstance(levels, str): + # Check for clone + if levels.startswith("(") and levels.endswith(")"): + if (clone := levels[1:-1]) and clone in LAYER_KEYS: + for layer, keys in self.layers.items(): + if value := keys.get(clone): + self.layers[layer][key] = value + continue + raise ValueError(f"Unsupported key mapping: {raw_key}: {levels}") + for raw_layer, raw_value in levels.items(): + value = self._parse_value(raw_value) + if (layer := Layer.parse(raw_layer)) is None: + raise ValueError(f"Cannot parse layer: “{raw_layer}”") + else: + self.layers[layer][key] = value + + def _parse_dead_keys(self, spc: Dict[str, str]) -> None: """Build a deadkey dict.""" @@ -281,29 +315,29 @@ def _parse_template( i = row.offset + col_offset keys = row.keys - base = list(template[2 + j * 3]) - shift = list(template[1 + j * 3]) + base = template[2 + j * 3] + shift = template[1 + j * 3] for key in keys: - base_key = ("*" if base[i - 1] == "*" else "") + base[i] - shift_key = ("*" if shift[i - 1] == "*" else "") + shift[i] + base_key: Optional[str] = self._parse_value(base[i-1:i+1], strip=True) + shift_key: Optional[str] = self._parse_value(shift[i-1:i+1], strip=True) # in the BASE layer, if the base character is undefined, shift prevails - if base_key == " ": - if layer_number == Layer.BASE: + if not base_key: + if layer_number is Layer.BASE and shift_key: base_key = shift_key.lower() # in other layers, if the shift character is undefined, base prevails - elif shift_key == " ": - if layer_number == Layer.ALTGR: + elif not shift_key: + if layer_number is Layer.ALTGR: shift_key = upper_key(base_key) - elif layer_number == Layer.ODK: + elif layer_number is Layer.ODK: shift_key = upper_key(base_key) # shift_key = upper_key(base_key, blank_if_obvious=False) - if base_key != " ": + if base_key: self.layers[layer_number][key] = base_key - if shift_key != " ": + if shift_key: self.layers[layer_number.next()][key] = shift_key for dk in DEAD_KEYS: @@ -338,33 +372,24 @@ def _fill_template( shift = list(template[1 + j * 3]) for key in keys: + indexes = slice(i - 1, i + 1) + base_key = " " if key in self.layers[layer_number]: - base_key = self.layers[layer_number][key] + base_key = SpecialSymbol.prettify(self.layers[layer_number][key]) shift_key = " " if key in self.layers[layer_number.next()]: - shift_key = self.layers[layer_number.next()][key] - - dead_base = len(base_key) == 2 and base_key[0] == "*" - dead_shift = len(shift_key) == 2 and shift_key[0] == "*" + shift_key = SpecialSymbol.prettify(self.layers[layer_number.next()][key]) if shift_prevails: - shift[i] = shift_key[-1] - if dead_shift: - shift[i - 1] = "*" - if upper_key(base_key) != shift_key: - base[i] = base_key[-1] - if dead_base: - base[i - 1] = "*" + shift[indexes] = shift_key.rjust(2) + if pretty_upper_key(base_key, blank_if_obvious=False) != shift_key: + base[indexes] = base_key.rjust(2) else: - base[i] = base_key[-1] - if dead_base: - base[i - 1] = "*" - if upper_key(base_key) != shift_key: - shift[i] = shift_key[-1] - if dead_shift: - shift[i - 1] = "*" + base[indexes] = base_key.rjust(2) + if pretty_upper_key(base_key, blank_if_obvious=False) != shift_key: + shift[indexes] = shift_key.rjust(2) i += 6 diff --git a/kalamine/utils.py b/kalamine/utils.py index dd06cce..5dcccba 100644 --- a/kalamine/utils.py +++ b/kalamine/utils.py @@ -1,12 +1,13 @@ import pkgutil from dataclasses import dataclass from enum import Enum, IntEnum, unique -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Self import yaml def hex_ord(char: str) -> str: + assert len(char) == 1, char return hex(ord(char))[2:].zfill(4) @@ -47,6 +48,24 @@ class Layer(IntEnum): ALTGR = 4 ALTGR_SHIFT = 5 + @classmethod + def parse(cls, raw: str) -> Self | None: + match raw.casefold(): + case "1dk": + return cls(cls.ODK) + case "1dk_shift": + return cls(cls.ODK_SHIFT) + case _: + for l in cls: + if raw.casefold() == l.name.casefold(): + return l + try: + if int(raw, base=10) == l.value: + return l + except: + pass + return None + def next(self) -> "Layer": """The next layer in the layer ordering.""" return Layer(int(self) + 1) @@ -60,16 +79,13 @@ def necromance(self) -> "Layer": return self -def upper_key(letter: Optional[str], blank_if_obvious: bool = True) -> str: - """This is used for presentation purposes: in a key, the upper character - becomes blank if it's an obvious uppercase version of the base character.""" - - if letter is None: - return " " +def upper_key(letter: Optional[str]) -> Optional[str]: + if not letter: + return None - special_symbols = {s.value: s.value for s in SpecialSymbol} + special_symbols = {s.value for s in SystemSymbol} if letter in special_symbols: - return special_symbols[letter] + return letter custom_alpha = { "\u00df": "\u1e9e", # ß ẞ @@ -81,16 +97,27 @@ def upper_key(letter: Optional[str], blank_if_obvious: bool = True) -> str: "\u2191": "\u21d1", # ↑ ⇑ "\u2192": "\u21d2", # → ⇒ "\u2193": "\u21d3", # ↓ ⇓ - "\u00b5": " ", # µ (to avoid getting `Μ` as uppercase) + # FIXME: strange behavior + "\u00b5": None, # µ (to avoid getting `Μ` as uppercase) } if letter in custom_alpha: return custom_alpha[letter] if len(letter) == 1 and letter.upper() != letter.lower(): return letter.upper() + + return None + + +def pretty_upper_key(letter: Optional[str], blank_if_obvious: bool = True) -> str: + """This is used for presentation purposes: in a key, the upper character + becomes blank if it's an obvious uppercase version of the base character.""" + + if (letterʹ := upper_key(letter)) is None: + return " " # dead key or non-letter character - return " " if blank_if_obvious else letter + return " " if blank_if_obvious else letterʹ @dataclass @@ -114,14 +141,43 @@ class DeadKeyDescr: ODK_ID = "**" # must match the value in dead_keys.yaml @unique -class SpecialSymbol(Enum): +class SystemSymbol(Enum): Alt = "⎇" AltGr = "⇮" + BackSpace = "⌫" CapsLock = "⇬" Compose = "⎄" Control = "⎈" + Escape = "⎋" + Return = "⏎" Shift = "⇧" +@dataclass +class SpecialSymbolEntry: + value: str + pretty: str + +class SpecialSymbol(Enum): + NarrowNoBreakSpace = SpecialSymbolEntry("\u202F", "n⍽") + NoBreakSpace = SpecialSymbolEntry("\u00A0", "⍽") + Space = SpecialSymbolEntry(" ", "␣") + + @classmethod + def parse(cls, raw: str) -> str: + for s in cls: + if raw == s.value.pretty: + return s.value.value + else: + return raw + + @classmethod + def prettify(cls, raw: str) -> str: + for s in cls: + if raw == s.value.value: + return s.value.pretty + else: + return raw + LAYER_KEYS = [ "- Digits", "ae01", @@ -189,4 +245,9 @@ class SpecialSymbol(Enum): "rctl", "muhe", "henk", + "- Miscellaneous", + "i172", + "rtrn", + "bksp", + "esc", ] From 75463d945b80ec0327ade730311b6de79788a86f Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Sat, 6 Apr 2024 18:06:53 +0200 Subject: [PATCH 03/17] XKB strings --- kalamine/cli.py | 12 ++-- kalamine/generators/xkb.py | 114 +++++++++++++++++++++++++++++++++---- kalamine/layout.py | 20 ++++++- kalamine/utils.py | 13 ++++- 4 files changed, 138 insertions(+), 21 deletions(-) diff --git a/kalamine/cli.py b/kalamine/cli.py index 34c5cd0..ef52b96 100644 --- a/kalamine/cli.py +++ b/kalamine/cli.py @@ -61,13 +61,11 @@ def file_creation_context(ext: str = "") -> Iterator[Path]: # Linux driver, user-space with file_creation_context(".xkb_keymap") as xkb_path: - with xkb_path.open("w", encoding="utf-8", newline="\n") as file: - file.write(xkb.xkb_keymap(layout)) + xkb.xkb_write_files(xkb_path, xkb.xkb_keymap(layout)) # Linux driver, root with file_creation_context(".xkb_symbols") as xkb_custom_path: - with xkb_custom_path.open("w", encoding="utf-8", newline="\n") as file: - file.write(xkb.xkb_symbols(layout)) + xkb.xkb_write_files(xkb_custom_path, xkb.xkb_symbols(layout)) # JSON data with file_creation_context(".json") as json_path: @@ -141,12 +139,10 @@ def build( file.write(keylayout.keylayout(layout)) elif output_file.suffix == ".xkb_keymap": - with output_file.open("w", encoding="utf-8", newline="\n") as file: - file.write(xkb.xkb_keymap(layout)) + xkb.xkb_write_files(output_file, xkb.xkb_keymap(layout)) elif output_file.suffix == ".xkb_symbols": - with output_file.open("w", encoding="utf-8", newline="\n") as file: - file.write(xkb.xkb_symbols(layout)) + xkb.xkb_write_files(output_file, xkb.xkb_symbols(layout)) elif output_file.suffix == ".json": output_file.write_text(web.pretty_json(layout), encoding="utf8") diff --git a/kalamine/generators/xkb.py b/kalamine/generators/xkb.py index efdd117..2bb9691 100644 --- a/kalamine/generators/xkb.py +++ b/kalamine/generators/xkb.py @@ -4,7 +4,10 @@ - xkb symbols/patch for XOrg (system-wide) & Wayland (system-wide/user-space) """ -from typing import TYPE_CHECKING, List +from dataclasses import dataclass +import itertools +from pathlib import Path +from typing import TYPE_CHECKING, Dict, Generator, List, Optional if TYPE_CHECKING: from ..layout import KeyboardLayout @@ -28,8 +31,51 @@ tuple(s for s in SystemSymbol if s.value not in XKB_SPECIAL_KEYSYMS) XKB_KEY_SYM.update(XKB_SPECIAL_KEYSYMS) +SPARE_KEYSYMS = ( + "F20", + "F21", + "F22", + "F23", + "F24", + "F25", + "F26", + "F27", + "F28", + "F29", + "F30", + "F31", + "F32", + "F33", + "F34", + "F35", +) + + +@dataclass +class XKB_Output: + symbols: str + compose: str + + +def xkb_make_strings(layout: "KeyboardLayout") -> Dict[str, str]: + layoutSymbols = layout.symbols + forbiden = set(itertools.chain(layoutSymbols.strings, layoutSymbols.deadKeys)) + spares = list(SPARE_KEYSYMS) + mapping = {} + for s in layoutSymbols.strings: + if len(s) >= 2: + # Try to use one of the characters of the string + if candidates := tuple((cʹ, keysym) for c in s for cʹ in (c.lower(), c.upper()) if len(cʹ) == 1 and cʹ not in forbiden and (keysym := XKB_KEY_SYM.get(cʹ))): + mapping[s] = candidates[0][1] + forbiden.add(candidates[0][0]) + elif spares: + mapping[s] = spares.pop(0) + else: + raise ValueError(f"Cannot encode string: “{s}”") + return mapping + -def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False) -> List[str]: +def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, strings: Optional[Dict[str, str]]=None) -> List[str]: """GNU/Linux layout.""" if layout.qwerty_shortcuts: @@ -40,6 +86,9 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False) -> List[str]: odk_symbol = "ISO_Level5_Latch" if eight_level else "ISO_Level3_Latch" max_length = 16 # `ISO_Level3_Latch` should be the longest symbol name + if strings is None: + strings = {} + output: List[str] = [] for key_name in LAYER_KEYS: if key_name.startswith("-"): # separator @@ -54,8 +103,10 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False) -> List[str]: if key_name in layer: keysym = layer[key_name] desc = keysym + if keysymʹ := strings.get(keysym): + symbol = keysymʹ # dead key? - if keysym in DK_INDEX: + elif keysym in DK_INDEX: name = DK_INDEX[keysym].name desc = layout.dead_keys[keysym][keysym] symbol = odk_symbol if keysym == ODK_ID else f"dead_{name}" @@ -110,17 +161,58 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False) -> List[str]: return output -def xkb_keymap(self) -> str: # will not work with Wayland +def xkb_keymap(layout: "KeyboardLayout") -> XKB_Output: # will not work with Wayland """GNU/Linux driver (standalone / user-space)""" - out = load_tpl(self, ".xkb_keymap") - out = substitute_lines(out, "LAYOUT", xkb_table(self, xkbcomp=True)) - return out + strings = xkb_make_strings(layout) + + symbols = load_tpl(layout, ".xkb_keymap") + symbols = substitute_lines(symbols, "LAYOUT", xkb_table(layout, xkbcomp=True, strings=strings)) + + compose = "\n".join(xcompose(strings)) if strings else "" + + return XKB_Output(symbols, compose) -def xkb_symbols(self) -> str: +def xkb_symbols(layout: "KeyboardLayout") -> XKB_Output: """GNU/Linux driver (xkb patch, system or user-space)""" - out = load_tpl(self, ".xkb_symbols") - out = substitute_lines(out, "LAYOUT", xkb_table(self, xkbcomp=False)) - return out.replace("//#", "//") + strings = xkb_make_strings(layout) + + symbols = load_tpl(layout, ".xkb_symbols") + symbols = substitute_lines(symbols, "LAYOUT", xkb_table(layout, xkbcomp=False, strings=strings)) + + compose = "\n".join(xcompose(strings)) if strings else "" + + return XKB_Output(symbols.replace("//#", "//"), compose) + + +def escapeString(s: str) -> Generator[str, None, None]: + for c in s: + if (cp := ord(c)) < 0x20: + yield f"\\x{cp:0>2x}" + match c: + case "\\": + yield "\\\\" + case "\"": + yield "\\\"" + case _: + # FIXME escape all relevant chars + yield c + + +def xcompose(strings: Dict[str, str]) -> Generator[str, None, None]: + for s, keysym in strings.items(): + s = "".join(escapeString(s)) + yield f"<{keysym}> : \"{s}\"" + yield "" + + +def xkb_write_files(path: Path, result: XKB_Output): + with path.open("w", encoding="utf-8", newline="\n") as file: + file.write(result.symbols) + if result.compose is None: + return + path = path.with_suffix(".xkb_compose") + with path.open("w", encoding="utf-8", newline="\n") as file: + file.write(result.compose) \ No newline at end of file diff --git a/kalamine/layout.py b/kalamine/layout.py index 9d1c6ff..3818d48 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -125,6 +125,12 @@ def from_dict(cls: Type[T], src: Dict) -> T: } +@dataclass +class LayoutSymbols: + strings: Set[str] + deadKeys: Set[str] + + ### # Main # @@ -408,7 +414,7 @@ def _get_geometry(self, layers: Optional[List[Layer]] = None) -> List[str]: for i in layers: template = self._fill_template(template, rows, i) return template - + @property def geometry(self) -> str: """ANSI, ISO, ERGO.""" @@ -436,3 +442,15 @@ def full(self) -> List[str]: def altgr(self) -> List[str]: """AltGr layer only.""" return self._get_geometry([Layer.ALTGR]) + + @property + def symbols(self) -> LayoutSymbols: + strings = set() + deadKeys = set() + for levels in self.layers.values(): + for value in levels.values(): + if len(value) == 2 and value[0] == "*": + deadKeys.add(value[1]) + elif SystemSymbol.parse(value) is None: + strings.add(value) + return LayoutSymbols(strings, deadKeys) diff --git a/kalamine/utils.py b/kalamine/utils.py index 5dcccba..1ad65f0 100644 --- a/kalamine/utils.py +++ b/kalamine/utils.py @@ -7,7 +7,10 @@ def hex_ord(char: str) -> str: - assert len(char) == 1, char + # assert len(char) == 1, char + if len(char) != 1: + print(f"ERROR: hex_ord: “{char}”") + char = char[0] return hex(ord(char))[2:].zfill(4) @@ -152,6 +155,14 @@ class SystemSymbol(Enum): Return = "⏎" Shift = "⇧" + @classmethod + def parse(cls, raw: str) -> Self: + for s in cls: + if raw == s.value: + return s + else: + return None + @dataclass class SpecialSymbolEntry: value: str From 64c93c53d2c4ab76bdf54c6715a35da44c40f830 Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Sat, 6 Apr 2024 20:00:15 +0200 Subject: [PATCH 04/17] Custom dead keys --- kalamine/data/dead_keys.yaml | 2 +- kalamine/generators/keylayout.py | 22 +++---- kalamine/generators/klc.py | 18 +++--- kalamine/generators/xkb.py | 106 ++++++++++++++++++++++++------- kalamine/layout.py | 67 ++++++++++++++++--- kalamine/utils.py | 15 ++++- 6 files changed, 176 insertions(+), 54 deletions(-) diff --git a/kalamine/data/dead_keys.yaml b/kalamine/data/dead_keys.yaml index 5cdf6c8..aadadeb 100644 --- a/kalamine/data/dead_keys.yaml +++ b/kalamine/data/dead_keys.yaml @@ -31,7 +31,7 @@ - char: '*‟' # there’s no “double grave accent” Unicode character :( name: doublegrave - base: AaEeIiOoRrUuѴѴ + base: AaEeIiOoRrUuѴѵ alt: ȀȁȄȅȈȉȌȍȐȑȔȕѶѷ alt_space: '‟' # U+201F HIGH REVERSED-9 QUOTATION MARK alt_self: '‟' # U+201F HIGH REVERSED-9 QUOTATION MARK diff --git a/kalamine/generators/keylayout.py b/kalamine/generators/keylayout.py index 501f962..855262b 100644 --- a/kalamine/generators/keylayout.py +++ b/kalamine/generators/keylayout.py @@ -9,7 +9,7 @@ from ..layout import KeyboardLayout from ..template import load_tpl, substitute_lines -from ..utils import DK_INDEX, LAYER_KEYS, SCAN_CODES, Layer, hex_ord +from ..utils import LAYER_KEYS, SCAN_CODES, Layer, hex_ord def _xml_proof(char: str) -> str: @@ -62,7 +62,7 @@ def has_dead_keys(letter: str) -> bool: if key_name in layer: key = layer[key_name] if key in layout.dead_keys: - symbol = f"dead_{DK_INDEX[key].name}" + symbol = f"dead_{layout.custom_dead_keys[key].name}" final_key = False else: symbol = _xml_proof(key.upper() if caps else key) @@ -89,7 +89,7 @@ def macos_actions(layout: "KeyboardLayout") -> List[str]: def when(state: str, action: str) -> str: state_attr = f'state="{state}"'.ljust(18) if action in layout.dead_keys: - action_attr = f'next="{DK_INDEX[action].name}"' + action_attr = f'next="{layout.custom_dead_keys[action].name}"' elif action.startswith("dead_"): action_attr = f'next="{action[5:]}"' else: @@ -105,12 +105,12 @@ def append_actions(key: str, symbol: str, actions: List[Tuple[str, str]]) -> Non # dead key definitions for key in layout.dead_keys: - name = DK_INDEX[key].name + name = layout.custom_dead_keys[key].name term = layout.dead_keys[key][key] ret_actions.append(f'') ret_actions.append(f' ') if name == "1dk" and term in layout.dead_keys: - nested_dk = DK_INDEX[term].name + nested_dk = layout.custom_dead_keys[term].name ret_actions.append(f' ') ret_actions.append("") continue @@ -133,18 +133,18 @@ def append_actions(key: str, symbol: str, actions: List[Tuple[str, str]]) -> Non continue actions: List[Tuple[str, str]] = [] - for k in DK_INDEX: + for k in layout.custom_dead_keys: if k in layout.dead_keys: if key in layout.dead_keys[k]: - actions.append((DK_INDEX[k].name, layout.dead_keys[k][key])) + actions.append((layout.custom_dead_keys[k].name, layout.dead_keys[k][key])) if actions: append_actions(key_name, _xml_proof(key), actions) # spacebar actions actions = [] - for k in DK_INDEX: + for k in layout.custom_dead_keys: if k in layout.dead_keys: - actions.append((DK_INDEX[k].name, layout.dead_keys[k][" "])) + actions.append((layout.custom_dead_keys[k].name, layout.dead_keys[k][" "])) append_actions("spce", " ", actions) # space append_actions("spce", " ", actions) # no-break space append_actions("spce", " ", actions) # fine no-break space @@ -156,11 +156,11 @@ def macos_terminators(layout: "KeyboardLayout") -> List[str]: """macOS layout, dead key terminators.""" ret_terminators = [] - for key in DK_INDEX: + for key in layout.custom_dead_keys: if key not in layout.dead_keys: continue dk = layout.dead_keys[key] - name = DK_INDEX[key].name + name = layout.custom_dead_keys[key].name term = dk[key] if name == "1dk" and term in layout.dead_keys: term = dk[" "] diff --git a/kalamine/generators/klc.py b/kalamine/generators/klc.py index 329157c..96fcc64 100644 --- a/kalamine/generators/klc.py +++ b/kalamine/generators/klc.py @@ -17,7 +17,7 @@ from ..layout import KeyboardLayout from ..template import load_tpl, substitute_lines, substitute_token -from ..utils import DK_INDEX, LAYER_KEYS, SCAN_CODES, Layer, hex_ord, load_data +from ..utils import LAYER_KEYS, SCAN_CODES, Layer, hex_ord, load_data # return the corresponding char for a symbol @@ -181,12 +181,12 @@ def klc_deadkeys(layout: "KeyboardLayout") -> List[str]: output = [] - for k in DK_INDEX: + for k in layout.custom_dead_keys: if k not in layout.dead_keys: continue dk = layout.dead_keys[k] - output.append(f"// DEADKEY: {DK_INDEX[k].name.upper()} //" + "{{{") + output.append(f"// DEADKEY: {layout.custom_dead_keys[k].name.upper()} //" + "{{{") output.append(f"DEADKEY\t{hex_ord(dk[' '])}") for base, alt in dk.items(): @@ -214,11 +214,11 @@ def klc_dk_index(layout: "KeyboardLayout") -> List[str]: """Windows layout, dead key index.""" output = [] - for k in DK_INDEX: + for k in layout.custom_dead_keys: if k not in layout.dead_keys: continue dk = layout.dead_keys[k] - output.append(f"{hex_ord(dk[' '])}\t\"{DK_INDEX[k].name.upper()}\"") + output.append(f"{hex_ord(dk[' '])}\t\"{layout.custom_dead_keys[k].name.upper()}\"") return output @@ -327,12 +327,12 @@ def c_deadkeys(layout: "KeyboardLayout") -> List[str]: output = [] - for k in DK_INDEX: + for k in layout.custom_dead_keys: if k not in layout.dead_keys: continue dk = layout.dead_keys[k] - output.append(f"// DEADKEY: {DK_INDEX[k].name.upper()}") + output.append(f"// DEADKEY: {layout.custom_dead_keys[k].name.upper()}") for base, alt in dk.items(): if base == k and alt in base: @@ -361,11 +361,11 @@ def c_dk_index(layout: "KeyboardLayout") -> List[str]: """Windows layout, dead key index.""" output = [] - for k in DK_INDEX: + for k in layout.custom_dead_keys: if k not in layout.dead_keys: continue term = layout.dead_keys[k][" "] - output.append(f'L"\\\\x{hex_ord(term)}"\tL"{DK_INDEX[k].name.upper()}",') + output.append(f'L"\\\\x{hex_ord(term)}"\tL"{layout.custom_dead_keys[k].name.upper()}",') return output diff --git a/kalamine/generators/xkb.py b/kalamine/generators/xkb.py index 2bb9691..4ff88ac 100644 --- a/kalamine/generators/xkb.py +++ b/kalamine/generators/xkb.py @@ -7,13 +7,13 @@ from dataclasses import dataclass import itertools from pathlib import Path -from typing import TYPE_CHECKING, Dict, Generator, List, Optional +from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Tuple if TYPE_CHECKING: from ..layout import KeyboardLayout from ..template import load_tpl, substitute_lines -from ..utils import DK_INDEX, LAYER_KEYS, ODK_ID, SystemSymbol, hex_ord, load_data +from ..utils import DK_INDEX, LAYER_KEYS, ODK_ID, DeadKeyDescr, SystemSymbol, hex_ord, load_data XKB_KEY_SYM = load_data("key_sym") XKB_SPECIAL_KEYSYMS = { @@ -50,6 +50,11 @@ "F35", ) +@dataclass +class XKB_Custom_Keysyms: + strings: Dict[str, str] + deadKeys: Dict[str, str] + @dataclass class XKB_Output: @@ -57,25 +62,45 @@ class XKB_Output: compose: str -def xkb_make_strings(layout: "KeyboardLayout") -> Dict[str, str]: +def xkb_make_custom_dead_keys_keysyms(layout: "KeyboardLayout") -> XKB_Custom_Keysyms: layoutSymbols = layout.symbols forbiden = set(itertools.chain(layoutSymbols.strings, layoutSymbols.deadKeys)) spares = list(SPARE_KEYSYMS) - mapping = {} + strings = {} + deadKeys = {} for s in layoutSymbols.strings: if len(s) >= 2: # Try to use one of the characters of the string if candidates := tuple((cʹ, keysym) for c in s for cʹ in (c.lower(), c.upper()) if len(cʹ) == 1 and cʹ not in forbiden and (keysym := XKB_KEY_SYM.get(cʹ))): - mapping[s] = candidates[0][1] + strings[s] = candidates[0][1] forbiden.add(candidates[0][0]) elif spares: - mapping[s] = spares.pop(0) + strings[s] = spares.pop(0) else: raise ValueError(f"Cannot encode string: “{s}”") - return mapping + for c in layoutSymbols.deadKeys: + dk = f"*{c}" + if dk in DK_INDEX: + continue + elif c not in forbiden and (keysym := XKB_KEY_SYM.get(c)): + deadKeys[dk] = keysym + forbiden.add(c) + elif spares: + deadKeys[dk] = spares.pop(0) + else: + raise ValueError(f"Cannot encode dead key: “{dk}”") + + return XKB_Custom_Keysyms(strings, deadKeys) -def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, strings: Optional[Dict[str, str]]=None) -> List[str]: +def xkb_keysym(char: str, max_length: Optional[int] = None) -> str: + if char in XKB_KEY_SYM and (max_length is None or len(XKB_KEY_SYM[char]) <= max_length): + return XKB_KEY_SYM[char] + else: + return f"U{hex_ord(char).upper()}" + + +def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, customDeadKeys: Optional[XKB_Custom_Keysyms]=None) -> List[str]: """GNU/Linux layout.""" if layout.qwerty_shortcuts: @@ -86,8 +111,8 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, strings: Optional odk_symbol = "ISO_Level5_Latch" if eight_level else "ISO_Level3_Latch" max_length = 16 # `ISO_Level3_Latch` should be the longest symbol name - if strings is None: - strings = {} + if customDeadKeys is None: + customDeadKeys = XKB_Custom_Keysyms({}, {}) output: List[str] = [] for key_name in LAYER_KEYS: @@ -103,11 +128,15 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, strings: Optional if key_name in layer: keysym = layer[key_name] desc = keysym - if keysymʹ := strings.get(keysym): + if keysymʹ := customDeadKeys.strings.get(keysym): symbol = keysymʹ - # dead key? + elif keysymʹ := customDeadKeys.deadKeys.get(keysym): + name = layout.custom_dead_keys[keysym].name + desc = layout.dead_keys[keysym][keysym] + symbol = keysymʹ + # predefined dead key? elif keysym in DK_INDEX: - name = DK_INDEX[keysym].name + name = layout.custom_dead_keys[keysym].name desc = layout.dead_keys[keysym][keysym] symbol = odk_symbol if keysym == ODK_ID else f"dead_{name}" # regular key: use a keysym if possible, utf-8 otherwise @@ -164,12 +193,12 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, strings: Optional def xkb_keymap(layout: "KeyboardLayout") -> XKB_Output: # will not work with Wayland """GNU/Linux driver (standalone / user-space)""" - strings = xkb_make_strings(layout) + customDeadKeysKeysyms = xkb_make_custom_dead_keys_keysyms(layout) symbols = load_tpl(layout, ".xkb_keymap") - symbols = substitute_lines(symbols, "LAYOUT", xkb_table(layout, xkbcomp=True, strings=strings)) + symbols = substitute_lines(symbols, "LAYOUT", xkb_table(layout, xkbcomp=True, customDeadKeys=customDeadKeysKeysyms)) - compose = "\n".join(xcompose(strings)) if strings else "" + compose = "\n".join(xcompose(layout, customDeadKeysKeysyms)) if customDeadKeysKeysyms else "" return XKB_Output(symbols, compose) @@ -177,12 +206,12 @@ def xkb_keymap(layout: "KeyboardLayout") -> XKB_Output: # will not work with Wa def xkb_symbols(layout: "KeyboardLayout") -> XKB_Output: """GNU/Linux driver (xkb patch, system or user-space)""" - strings = xkb_make_strings(layout) + customDeadKeysKeysyms = xkb_make_custom_dead_keys_keysyms(layout) symbols = load_tpl(layout, ".xkb_symbols") - symbols = substitute_lines(symbols, "LAYOUT", xkb_table(layout, xkbcomp=False, strings=strings)) + symbols = substitute_lines(symbols, "LAYOUT", xkb_table(layout, xkbcomp=False, customDeadKeys=customDeadKeysKeysyms)) - compose = "\n".join(xcompose(strings)) if strings else "" + compose = "\n".join(xcompose(layout, customDeadKeysKeysyms)) if customDeadKeysKeysyms else "" return XKB_Output(symbols.replace("//#", "//"), compose) @@ -201,13 +230,46 @@ def escapeString(s: str) -> Generator[str, None, None]: yield c -def xcompose(strings: Dict[str, str]) -> Generator[str, None, None]: - for s, keysym in strings.items(): +def _compose_sequences(dk: DeadKeyDescr) -> Generator[Tuple[str, str], None, None]: + yield from zip(dk.base, dk.alt) + yield (dk.char, dk.alt_self) + yield (" ", dk.alt_space) + + +def xcompose(layout: "KeyboardLayout", customDeadKeys: XKB_Custom_Keysyms) -> Generator[str, None, None]: + # Strings + for s, keysym in customDeadKeys.strings.items(): s = "".join(escapeString(s)) yield f"<{keysym}> : \"{s}\"" + # Dead keys sequences + for dk in layout.custom_dead_keys.values(): + if dk.char == ODK_ID: + continue + # Predefined dk + if (dkʹ := DK_INDEX.get(dk.char)): + predefined = dict(_compose_sequences(dkʹ)) + else: + predefined = {} + # Dead key keysym + if (dk_keysym := customDeadKeys.deadKeys.get(dk.char)) is None: + # dk_keysym = xkb_keysym(dk.char[-1]) + dk_keysym = f"dead_{dk.name}" + # Sequences + for base, result in _compose_sequences(dk): + if predefined.get(base) == result: + # Skip predefined sequence + continue + print(dk.name, base, result) + # TODO: general chained dead keys? + if base == dk.char: + base_keysym = dk_keysym + else: + base_keysym = xkb_keysym(base) + result_keysym = xkb_keysym(result) + result_string = "".join(escapeString(result)) + yield f"<{dk_keysym}> <{base_keysym}> : \"{result_string}\" {result_keysym}" yield "" - def xkb_write_files(path: Path, result: XKB_Output): with path.open("w", encoding="utf-8", newline="\n") as file: file.write(result.symbols) diff --git a/kalamine/layout.py b/kalamine/layout.py index 3818d48..239f6f4 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -1,8 +1,9 @@ import copy +import itertools import sys from dataclasses import dataclass from pathlib import Path -from typing import Dict, List, Optional, Set, Type, TypeVar +from typing import Any, Dict, List, Optional, Set, Type, TypeVar import click import tomli @@ -10,8 +11,10 @@ from .utils import ( DEAD_KEYS, + DK_INDEX, LAYER_KEYS, ODK_ID, + DeadKeyDescr, Layer, SpecialSymbol, SystemSymbol, @@ -170,6 +173,9 @@ def __init__( ) self.meta["fileName"] = self.meta["name8"].lower() + # Custom dead keys + self.custom_dead_keys = self._parse_dead_keys(layout_data.get("dead_keys", {})) + # keyboard layers: self.layers & self.dead_keys rows = copy.deepcopy(GEOMETRY[self.meta["geometry"]].rows) @@ -200,6 +206,7 @@ def __init__( ) # space bar + # FIXME: dead key in space bar? spc = SPACEBAR.copy() if "spacebar" in layout_data: for k in layout_data["spacebar"]: @@ -229,7 +236,43 @@ def __init__( if key not in keys: keys[key] = base_symbol - self._parse_dead_keys(spc) + self._make_dead_keys(spc) + + def _parse_dead_keys(self, raw: Dict[str, Any]) -> Dict[str, DeadKeyDescr]: + custom_dead_keys: Dict[str, DeadKeyDescr] = DK_INDEX.copy() + for dk_char, definition in raw.items(): + if dk_char == ODK_ID: + raise ValueError("Cannot redefine 1dk") + name = definition.get("name") + base = definition.get("base") + alt = definition.get("alt") + alt_space = definition.get("alt_space") + alt_self = definition.get("alt_self") + if dk := DK_INDEX.get(dk_char): + # Redefine existing predefined dead key + mapping = dict(zip(dk.base, dk.alt)) + if base and alt: + mapping.update(dict(zip(base, alt))) + custom_dead_keys[dk_char] = DeadKeyDescr( + char=dk_char, + name=name or dk.name, + base="".join(mapping.keys()), + alt="".join(mapping.values()), + alt_space=alt_space or dk.alt_space, + alt_self=alt_self or dk.alt_self + ) + elif not dk_char or not name or not base or not alt or not alt_space or not alt_self: + raise ValueError(f"Invalid custom dead key definition: {definition}") + else: + custom_dead_keys[dk_char] = DeadKeyDescr( + char=dk_char, + name=name, + base=base, + alt=alt, + alt_space=alt_space, + alt_self=alt_self + ) + return custom_dead_keys def _parse_value(self, raw: str, strip=False) -> str: return SpecialSymbol.parse(raw.strip() if strip else raw) @@ -258,8 +301,7 @@ def _parse_mapping(self, mapping: Dict[str, str | Dict[str, str]]): else: self.layers[layer][key] = value - - def _parse_dead_keys(self, spc: Dict[str, str]) -> None: + def _make_dead_keys(self, spc: Dict[str, str]) -> None: """Build a deadkey dict.""" def layout_has_char(char: str) -> bool: @@ -279,7 +321,7 @@ def layout_has_char(char: str) -> bool: all_spaces.append(space) self.dead_keys = {} - for dk in DEAD_KEYS: + for dk in itertools.chain(DEAD_KEYS, self.custom_dead_keys.values()): id = dk.char if id not in self.dk_set: continue @@ -343,12 +385,19 @@ def _parse_template( if base_key: self.layers[layer_number][key] = base_key + if DeadKeyDescr.is_dead_key(base_key): + if (base_key in DK_INDEX) or (base_key in self.custom_dead_keys): + self.dk_set.add(base_key) + else: + raise ValueError(f"Undefined dead key: {base_key}") + if shift_key: self.layers[layer_number.next()][key] = shift_key - - for dk in DEAD_KEYS: - if base_key == dk.char or shift_key == dk.char: - self.dk_set.add(dk.char) + if DeadKeyDescr.is_dead_key(shift_key): + if (shift_key in DK_INDEX) or (shift_key in self.custom_dead_keys): + self.dk_set.add(shift_key) + else: + raise ValueError(f"Undefined dead key: {shift_key}") i += 6 j += 1 diff --git a/kalamine/utils.py b/kalamine/utils.py index 1ad65f0..38c6ce1 100644 --- a/kalamine/utils.py +++ b/kalamine/utils.py @@ -132,10 +132,21 @@ class DeadKeyDescr: alt_space: str alt_self: str + @staticmethod + def is_dead_key(raw: str) -> bool: + return len(raw) == 2 and raw[0] == "*" + + @staticmethod + def parse_dead_key(raw: str) -> Optional[str]: + if len(raw) == 2 and raw[0] == "*": + return raw + else: + return None + DEAD_KEYS = [DeadKeyDescr(**data) for data in load_data("dead_keys")] -DK_INDEX = {} +DK_INDEX: Dict[str, DeadKeyDescr] = {} for dk in DEAD_KEYS: DK_INDEX[dk.char] = dk @@ -156,7 +167,7 @@ class SystemSymbol(Enum): Shift = "⇧" @classmethod - def parse(cls, raw: str) -> Self: + def parse(cls, raw: str) -> Optional[Self]: for s in cls: if raw == s.value: return s From 557eb520a63f539a0385b6407397bea58a9c097e Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Sun, 7 Apr 2024 13:55:29 +0200 Subject: [PATCH 05/17] Refactor scan codes and keys --- kalamine/data/scan_codes.yaml | 530 ++++++++++++++++++++----------- kalamine/generators/ahk.py | 54 ++-- kalamine/generators/keylayout.py | 68 ++-- kalamine/generators/klc.py | 36 +-- kalamine/generators/web.py | 36 +-- kalamine/generators/xkb.py | 79 +++-- kalamine/help.py | 11 +- kalamine/key.py | 108 +++++++ kalamine/layout.py | 18 +- kalamine/template.py | 3 - kalamine/utils.py | 76 ----- 11 files changed, 603 insertions(+), 416 deletions(-) create mode 100644 kalamine/key.py diff --git a/kalamine/data/scan_codes.yaml b/kalamine/data/scan_codes.yaml index 57333f4..bda3f00 100644 --- a/kalamine/data/scan_codes.yaml +++ b/kalamine/data/scan_codes.yaml @@ -1,194 +1,336 @@ -klc: - spce: '39' - - # digits - ae01: '02' - ae02: '03' - ae03: '04' - ae04: '05' - ae05: '06' - ae06: '07' - ae07: '08' - ae08: '09' - ae09: '0a' - ae10: '0b' - - # letters, first row - ad01: '10' - ad02: '11' - ad03: '12' - ad04: '13' - ad05: '14' - ad06: '15' - ad07: '16' - ad08: '17' - ad09: '18' - ad10: '19' - - # letters, second row - ac01: '1e' - ac02: '1f' - ac03: '20' - ac04: '21' - ac05: '22' - ac06: '23' - ac07: '24' - ac08: '25' - ac09: '26' - ac10: '27' - - # letters, third row - ab01: '2c' - ab02: '2d' - ab03: '2e' - ab04: '2f' - ab05: '30' - ab06: '31' - ab07: '32' - ab08: '33' - ab09: '34' - ab10: '35' - - # pinky keys - tlde: '29' - ae11: '0c' - ae12: '0d' - ae13: '0d' # XXX FIXME - ad11: '1a' - ad12: '1b' - ac11: '28' - ab11: '28' # XXX FIXME - bksl: '2b' - lsgt: '56' - -osx: - spce: 49 - - # digits - ae01: 18 # 1 - ae02: 19 # 2 - ae03: 20 # 3 - ae04: 21 # 4 - ae05: 23 # 5 - ae06: 22 # 6 - ae07: 26 # 7 - ae08: 28 # 8 - ae09: 25 # 9 - ae10: 29 # 0 - - # letters, first row - ad01: 12 # Q - ad02: 13 # W - ad03: 14 # E - ad04: 15 # R - ad05: 17 # T - ad06: 16 # Y - ad07: 32 # U - ad08: 34 # I - ad09: 31 # O - ad10: 35 # P - - # letters, second row - ac01: 0 # A - ac02: 1 # S - ac03: 2 # D - ac04: 3 # F - ac05: 5 # G - ac06: 4 # H - ac07: 38 # J - ac08: 40 # K - ac09: 37 # L - ac10: 41 # ★ - - # letters, third row - ab01: 6 # Z - ab02: 7 # X - ab03: 8 # C - ab04: 9 # V - ab05: 11 # B - ab06: 45 # N - ab07: 46 # M - ab08: 43 # , - ab09: 47 # . - ab10: 44 # / - - # pinky keys - tlde: 50 # ~ - ae11: 27 # - - ae12: 24 # = - ae13: 42 # XXX FIXME - ad11: 33 # [ - ad12: 30 # ] - ac11: 39 # ' - ab11: 39 # XXX FIXME - bksl: 42 # \ - lsgt: 10 # < - -web: - spce: 'Space' - - # digits - ae01: 'Digit1' - ae02: 'Digit2' - ae03: 'Digit3' - ae04: 'Digit4' - ae05: 'Digit5' - ae06: 'Digit6' - ae07: 'Digit7' - ae08: 'Digit8' - ae09: 'Digit9' - ae10: 'Digit0' - - # letters, 1st row - ad01: 'KeyQ' - ad02: 'KeyW' - ad03: 'KeyE' - ad04: 'KeyR' - ad05: 'KeyT' - ad06: 'KeyY' - ad07: 'KeyU' - ad08: 'KeyI' - ad09: 'KeyO' - ad10: 'KeyP' - - # letters, 2nd row - ac01: 'KeyA' - ac02: 'KeyS' - ac03: 'KeyD' - ac04: 'KeyF' - ac05: 'KeyG' - ac06: 'KeyH' - ac07: 'KeyJ' - ac08: 'KeyK' - ac09: 'KeyL' - ac10: 'Semicolon' - - # letters, 3rd row - ab01: 'KeyZ' - ab02: 'KeyX' - ab03: 'KeyC' - ab04: 'KeyV' - ab05: 'KeyB' - ab06: 'KeyN' - ab07: 'KeyM' - ab08: 'Comma' - ab09: 'Period' - ab10: 'Slash' - - # pinky keys - tlde: 'Backquote' - ae11: 'Minus' - ae12: 'Equal' - ae13: 'IntlYen' - ad11: 'BracketLeft' - ad12: 'BracketRight' - bksl: 'Backslash' - ac11: 'Quote' - ab11: 'IntlRo' - lsgt: 'IntlBackslash' - - # Other - lalt: 'AltLeft' - ralt: 'AltRight' - muhe: 'NonConvert' - henk: 'Convert' +digits: +- xkb: "ae01" + web: "Digit1" + windows: "02" + macos: "18" + hand: null +- xkb: "ae02" + web: "Digit2" + windows: "03" + macos: "19" + hand: null +- xkb: "ae03" + web: "Digit3" + windows: "04" + macos: "20" + hand: null +- xkb: "ae04" + web: "Digit4" + windows: "05" + macos: "21" + hand: null +- xkb: "ae05" + web: "Digit5" + windows: "06" + macos: "23" + hand: null +- xkb: "ae06" + web: "Digit6" + windows: "07" + macos: "22" + hand: null +- xkb: "ae07" + web: "Digit7" + windows: "08" + macos: "26" + hand: null +- xkb: "ae08" + web: "Digit8" + windows: "09" + macos: "28" + hand: null +- xkb: "ae09" + web: "Digit9" + windows: "0a" + macos: "25" + hand: null +- xkb: "ae10" + web: "Digit0" + windows: "0b" + macos: "29" + hand: null +# Letters, first row +Letters1: +- xkb: "ad01" + web: "KeyQ" + windows: "10" + macos: "12" + hand: null +- xkb: "ad02" + web: "KeyW" + windows: "11" + macos: "13" + hand: null +- xkb: "ad03" + web: "KeyE" + windows: "12" + macos: "14" + hand: null +- xkb: "ad04" + web: "KeyR" + windows: "13" + macos: "15" + hand: null +- xkb: "ad05" + web: "KeyT" + windows: "14" + macos: "17" + hand: null +- xkb: "ad06" + web: "KeyY" + windows: "15" + macos: "16" + hand: null +- xkb: "ad07" + web: "KeyU" + windows: "16" + macos: "32" + hand: null +- xkb: "ad08" + web: "KeyI" + windows: "17" + macos: "34" + hand: null +- xkb: "ad09" + web: "KeyO" + windows: "18" + macos: "31" + hand: null +- xkb: "ad10" + web: "KeyP" + windows: "19" + macos: "35" + hand: null +# Letters, second row +Letters2: +- xkb: "ac01" + web: "KeyA" + windows: "1e" + macos: "0" + hand: null +- xkb: "ac02" + web: "KeyS" + windows: "1f" + macos: "1" + hand: null +- xkb: "ac03" + web: "KeyD" + windows: "20" + macos: "2" + hand: null +- xkb: "ac04" + web: "KeyF" + windows: "21" + macos: "3" + hand: null +- xkb: "ac05" + web: "KeyG" + windows: "22" + macos: "5" + hand: null +- xkb: "ac06" + web: "KeyH" + windows: "23" + macos: "4" + hand: null +- xkb: "ac07" + web: "KeyJ" + windows: "24" + macos: "38" + hand: null +- xkb: "ac08" + web: "KeyK" + windows: "25" + macos: "40" + hand: null +- xkb: "ac09" + web: "KeyL" + windows: "26" + macos: "37" + hand: null +- xkb: "ac10" + web: "Semicolon" + windows: "27" + macos: "41" + hand: null +# Letters, third row +Letters3: +- xkb: "ab01" + web: "KeyZ" + windows: "2c" + macos: "6" + hand: null +- xkb: "ab02" + web: "KeyX" + windows: "2d" + macos: "7" + hand: null +- xkb: "ab03" + web: "KeyC" + windows: "2e" + macos: "8" + hand: null +- xkb: "ab04" + web: "KeyV" + windows: "2f" + macos: "9" + hand: null +- xkb: "ab05" + web: "KeyB" + windows: "30" + macos: "11" + hand: null +- xkb: "ab06" + web: "KeyN" + windows: "31" + macos: "45" + hand: null +- xkb: "ab07" + web: "KeyM" + windows: "32" + macos: "46" + hand: null +- xkb: "ab08" + web: "Comma" + windows: "33" + macos: "43" + hand: null +- xkb: "ab09" + web: "Period" + windows: "34" + macos: "47" + hand: null +- xkb: "ab10" + web: "Slash" + windows: "35" + macos: "44" + hand: null +# Pinky keys +PinkyKeys: +- xkb: "ae11" + web: "Minus" + windows: "0c" + macos: "27" + hand: null +- xkb: "ae12" + web: "Equal" + windows: "0d" + macos: "24" + hand: null +- xkb: "ae13" + web: "IntlYen" + windows: "0d" + macos: "42" + hand: null +- xkb: "ad11" + web: "BracketLeft" + windows: "1a" + macos: "33" + hand: null +- xkb: "ad12" + web: "BracketRight" + windows: "1b" + macos: "30" + hand: null +- xkb: "ac11" + web: "Quote" + windows: "28" + macos: "39" + hand: null +- xkb: "ab11" + web: "IntlRo" + windows: "28" + macos: "39" + hand: null +- xkb: "tlde" + web: "Backquote" + windows: "29" + macos: "50" + hand: null +- xkb: "bksl" + web: "Backslash" + windows: "2b" + macos: "42" + hand: null +- xkb: "lsgt" + web: "IntlBackslash" + windows: "56" + macos: "10" + hand: null +# Space bar row +SpaceBar: +- xkb: "spce" + web: "Space" + windows: "39" + macos: "49" + hand: null +System: +- xkb: "menu" + web: null + windows: null + macos: null + hand: null +- xkb: "rtrn" + web: null + windows: null + macos: null + hand: null +- xkb: "bksp" + web: null + windows: null + macos: null + hand: null +- xkb: "esc" + web: null + windows: null + macos: null + hand: null +Modifiers: +- xkb: "lfsh" + web: null + windows: null + macos: null + hand: Left +- xkb: "rtsh" + web: null + windows: null + macos: null + hand: Right +- xkb: "lalt" + web: "AltLeft" + windows: null + macos: null + hand: Left +- xkb: "ralt" + web: "AltRight" + windows: null + macos: null + hand: Right +- xkb: "lctl" + web: null + windows: null + macos: null + hand: Left +- xkb: "rctl" + web: null + windows: null + macos: null + hand: Right +InputMethod: +- xkb: "muhe" + web: "NonConvert" + windows: null + macos: null + hand: Left +- xkb: "henk" + web: "Convert" + windows: null + macos: Right + hand: null +# Miscellaneous +Miscellaneous: +- xkb: "i172" + web: null + windows: null + macos: null + hand: null \ No newline at end of file diff --git a/kalamine/generators/ahk.py b/kalamine/generators/ahk.py index 2d5f433..f564f7f 100644 --- a/kalamine/generators/ahk.py +++ b/kalamine/generators/ahk.py @@ -6,13 +6,14 @@ """ import json -from typing import TYPE_CHECKING, Dict, List +from typing import TYPE_CHECKING, Dict, List, Optional if TYPE_CHECKING: from ..layout import KeyboardLayout +from ..key import KEYS, KeyCategory from ..template import load_tpl, substitute_lines -from ..utils import LAYER_KEYS, SCAN_CODES, Layer, load_data +from ..utils import Layer, load_data def ahk_keymap(layout: "KeyboardLayout", altgr: bool = False) -> List[str]: @@ -38,32 +39,34 @@ def ahk_actions(symbol: str) -> Dict[str, str]: return actions output = [] - for key_name in LAYER_KEYS: - if key_name.startswith("-"): - output.append(f"; {key_name[1:]}") - output.append("") - continue - - # if key_name in ["ae13", "ab11"]: # ABNT / JIS keys + prev_category: Optional[KeyCategory] = None + for key in KEYS.values(): + # TODO: delete test? + # if key.id in ["ae13", "ab11"]: # ABNT / JIS keys # continue # these two keys are not supported yet - if key_name not in SCAN_CODES['klc']: + if key.windows is None: continue - sc = f"SC{SCAN_CODES['klc'][key_name]}" + if key.category is not prev_category: + output.append(f"; {key.category.description}") + output.append("") + prev_category = key.category + + sc = f"SC{key.windows}" for i in ( [Layer.ALTGR, Layer.ALTGR_SHIFT] if altgr else [Layer.BASE, Layer.SHIFT] ): layer = layout.layers[i] - if key_name not in layer: + if key.id not in layer: continue - symbol = layer[key_name] + symbol = layer[key.id] sym = ahk_escape(symbol) if symbol in layout.dead_keys: actions = {sym: layout.dead_keys[symbol][symbol]} - elif key_name == "spce": - actions = ahk_actions(key_name) + elif key.id == "spce": + actions = ahk_actions(key.id) else: actions = ahk_actions(symbol) @@ -85,24 +88,25 @@ def ahk_shortcuts(layout: "KeyboardLayout") -> List[str]: qwerty_vk = load_data("qwerty_vk") output = [] - for key_name in LAYER_KEYS: - if key_name.startswith("-"): - output.append(f"; {key_name[1:]}") - output.append("") - continue - + prev_category: Optional[KeyCategory] = None + for key in KEYS.values(): # if key_name in ["ae13", "ab11"]: # ABNT / JIS keys # continue # these two keys are not supported yet - if key_name not in SCAN_CODES['klc']: + if key.windows is None: continue - scan_code = SCAN_CODES["klc"][key_name] + if key.category is not prev_category: + output.append(f"; {key.category.description}") + output.append("") + prev_category = key.category + + scan_code = key.windows for i in [Layer.BASE, Layer.SHIFT]: layer = layout.layers[i] - if key_name not in layer: + if key.id not in layer: continue - symbol = layer[key_name] + symbol = layer[key.id] if layout.qwerty_shortcuts: symbol = qwerty_vk[scan_code] if symbol in enabled: diff --git a/kalamine/generators/keylayout.py b/kalamine/generators/keylayout.py index 855262b..5566501 100644 --- a/kalamine/generators/keylayout.py +++ b/kalamine/generators/keylayout.py @@ -3,13 +3,14 @@ https://developer.apple.com/library/content/technotes/tn2056/ """ -from typing import TYPE_CHECKING, List, Tuple +from typing import TYPE_CHECKING, List, Optional, Tuple if TYPE_CHECKING: from ..layout import KeyboardLayout +from ..key import KEYS, KeyCategory from ..template import load_tpl, substitute_lines -from ..utils import LAYER_KEYS, SCAN_CODES, Layer, hex_ord +from ..utils import Layer, hex_ord def _xml_proof(char: str) -> str: @@ -44,37 +45,39 @@ def has_dead_keys(letter: str) -> bool: return False output: List[str] = [] - for key_name in LAYER_KEYS: - if key_name in ["ae13", "ab11"]: # ABNT / JIS keys + prev_category: Optional[KeyCategory] = None + for key in KEYS.values(): + # TODO: remove test and use only next? + if key.id in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet - if key_name not in SCAN_CODES['osx']: + if key.macos is None: continue - if key_name.startswith("-"): + if key.category is not prev_category: if output: output.append("") - output.append("") - continue + output.append("") + prev_category = key.category symbol = "" final_key = True - if key_name in layer: - key = layer[key_name] - if key in layout.dead_keys: - symbol = f"dead_{layout.custom_dead_keys[key].name}" + if key.id in layer: + value = layer[key.id] + if value in layout.dead_keys: + symbol = f"dead_{layout.custom_dead_keys[value].name}" final_key = False else: - symbol = _xml_proof(key.upper() if caps else key) - final_key = not has_dead_keys(key.upper()) + symbol = _xml_proof(value.upper() if caps else value) + final_key = not has_dead_keys(value.upper()) - char = f"code=\"{SCAN_CODES['osx'][key_name]}\"".ljust(10) + char = f"code=\"{key.macos}\"".ljust(10) if final_key: action = f'output="{symbol}"' elif symbol.startswith("dead_"): action = f'action="{_xml_proof_id(symbol)}"' else: - action = f'action="{key_name}_{_xml_proof_id(symbol)}"' + action = f'action="{key.id}_{_xml_proof_id(symbol)}"' output.append(f"") ret_str.append(output) @@ -96,17 +99,17 @@ def when(state: str, action: str) -> str: action_attr = f'output="{_xml_proof(action)}"' return f" " - def append_actions(key: str, symbol: str, actions: List[Tuple[str, str]]) -> None: - ret_actions.append(f'') + def append_actions(id: str, symbol: str, actions: List[Tuple[str, str]]) -> None: + ret_actions.append(f'') ret_actions.append(when("none", symbol)) for state, out in actions: ret_actions.append(when(state, out)) ret_actions.append("") # dead key definitions - for key in layout.dead_keys: - name = layout.custom_dead_keys[key].name - term = layout.dead_keys[key][key] + for dk in layout.dead_keys: + name = layout.custom_dead_keys[dk].name + term = layout.dead_keys[dk][dk] ret_actions.append(f'') ret_actions.append(f' ') if name == "1dk" and term in layout.dead_keys: @@ -116,29 +119,30 @@ def append_actions(key: str, symbol: str, actions: List[Tuple[str, str]]) -> Non continue # normal key actions - for key_name in LAYER_KEYS: - if key_name.startswith("-"): + prev_category: Optional[KeyCategory] = None + for key in KEYS.values(): + if key.category is not prev_category: ret_actions.append("") - ret_actions.append(f"") - continue + ret_actions.append(f"") + prev_category = key.category for i in [Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT]: - if key_name == "spce" or key_name not in layout.layers[i]: + if key.id == "spce" or key.id not in layout.layers[i]: continue - key = layout.layers[i][key_name] - if i and key == layout.layers[Layer.BASE][key_name]: + value = layout.layers[i][key.id] + if i and value == layout.layers[Layer.BASE][key.id]: continue - if key in layout.dead_keys: + if value in layout.dead_keys: continue actions: List[Tuple[str, str]] = [] for k in layout.custom_dead_keys: if k in layout.dead_keys: - if key in layout.dead_keys[k]: - actions.append((layout.custom_dead_keys[k].name, layout.dead_keys[k][key])) + if value in layout.dead_keys[k]: + actions.append((layout.custom_dead_keys[k].name, layout.dead_keys[k][value])) if actions: - append_actions(key_name, _xml_proof(key), actions) + append_actions(key.id, _xml_proof(value), actions) # spacebar actions actions = [] diff --git a/kalamine/generators/klc.py b/kalamine/generators/klc.py index 96fcc64..6831d65 100644 --- a/kalamine/generators/klc.py +++ b/kalamine/generators/klc.py @@ -13,11 +13,13 @@ import re from typing import TYPE_CHECKING, List + if TYPE_CHECKING: from ..layout import KeyboardLayout +from ..key import KEYS from ..template import load_tpl, substitute_lines, substitute_token -from ..utils import LAYER_KEYS, SCAN_CODES, Layer, hex_ord, load_data +from ..utils import Layer, hex_ord, load_data # return the corresponding char for a symbol @@ -101,13 +103,11 @@ def klc_keymap(layout: "KeyboardLayout") -> List[str]: output = [] qwerty_vk = load_data("qwerty_vk") - for key_name in LAYER_KEYS: - if key_name.startswith("-"): - continue - - if key_name in ["ae13", "ab11"]: # ABNT / JIS keys + for key in KEYS.values(): + if key.id in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet - if key_name not in SCAN_CODES['klc']: + if key.windows is None: + # TODO: warning continue symbols = [] @@ -117,8 +117,8 @@ def klc_keymap(layout: "KeyboardLayout") -> List[str]: for i in [Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT]: layer = layout.layers[i] - if key_name in layer: - symbol = layer[key_name] + if key.id in layer: + symbol = layer[key.id] desc = symbol if symbol in layout.dead_keys: desc = layout.dead_keys[symbol][" "] @@ -134,7 +134,7 @@ def klc_keymap(layout: "KeyboardLayout") -> List[str]: symbols.append("-1") description += " " + desc - scan_code = SCAN_CODES["klc"][key_name] + scan_code = key.windows virtual_key = qwerty_vk[scan_code] if not layout.qwerty_shortcuts: @@ -231,13 +231,11 @@ def c_keymap(layout: "KeyboardLayout") -> List[str]: global oem_idx oem_idx = 0 # Python trick to do equivalent of C static variable output = [] - for key_name in LAYER_KEYS: - if key_name.startswith("-"): - continue - - if key_name in ["ae13", "ab11"]: # ABNT / JIS keys + for key in KEYS.values(): + if key.id in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet - if key_name not in SCAN_CODES['klc']: + if key.windows is None: + # TODO: warning continue symbols = [] @@ -248,8 +246,8 @@ def c_keymap(layout: "KeyboardLayout") -> List[str]: for i in [Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT]: layer = layout.layers[i] - if key_name in layer: - symbol = layer[key_name] + if key.id in layer: + symbol = layer[key.id] desc = symbol dead = "WCH_NONE" if symbol in layout.dead_keys: @@ -269,7 +267,7 @@ def c_keymap(layout: "KeyboardLayout") -> List[str]: symbols.append("WCH_NONE") dead_symbols.append("WCH_NONE") - scan_code = SCAN_CODES["klc"][key_name] + scan_code = key.windows virtual_key = qwerty_vk[scan_code] if not layout.qwerty_shortcuts: diff --git a/kalamine/generators/web.py b/kalamine/generators/web.py index 83ec2df..4fd1ab8 100644 --- a/kalamine/generators/web.py +++ b/kalamine/generators/web.py @@ -9,10 +9,12 @@ from typing import TYPE_CHECKING, Dict, List, Optional from xml.etree import ElementTree as ET + if TYPE_CHECKING: from ..layout import KeyboardLayout -from ..utils import LAYER_KEYS, ODK_ID, SCAN_CODES, Layer, pretty_upper_key +from ..key import KEYS +from ..utils import ODK_ID, Layer, pretty_upper_key def raw_json(layout: "KeyboardLayout") -> Dict: @@ -21,17 +23,16 @@ def raw_json(layout: "KeyboardLayout") -> Dict: # flatten the keymap: each key has an array of 2-4 characters # correcponding to Base, Shift, AltGr, AltGr+Shift keymap: Dict[str, List[str]] = {} - for key_name in LAYER_KEYS: - if key_name.startswith("-"): - continue - if key_name not in SCAN_CODES['web']: + for key in KEYS.values(): + if key.web is None: + # TODO: warning continue chars = list("") for i in [Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT]: - if key_name in layout.layers[i]: - chars.append(layout.layers[i][key_name]) + if key.id in layout.layers[i]: + chars.append(layout.layers[i][key.id]) if chars: - keymap[SCAN_CODES["web"][key_name]] = chars + keymap[key.web] = chars return { # fmt: off @@ -92,10 +93,9 @@ def same_symbol(key_name: str, lower: Layer, upper: Layer): return ET.ElementTree() svg = ET.ElementTree(ET.fromstring(res.decode("utf-8"))) - for key_name in LAYER_KEYS: - if key_name.startswith("-"): - continue - if key_name not in SCAN_CODES['web']: + for key in KEYS.values(): + if key.web is None: + # TODO: warning continue level = 0 @@ -108,16 +108,16 @@ def same_symbol(key_name: str, lower: Layer, upper: Layer): Layer.ODK_SHIFT, ]: level += 1 - if key_name not in layout.layers[i]: + if key.id not in layout.layers[i]: continue - if level == 1 and same_symbol(key_name, Layer.BASE, Layer.SHIFT): + if level == 1 and same_symbol(key.id, Layer.BASE, Layer.SHIFT): continue - if level == 4 and same_symbol(key_name, Layer.ALTGR, Layer.ALTGR_SHIFT): + if level == 4 and same_symbol(key.id, Layer.ALTGR, Layer.ALTGR_SHIFT): continue - if level == 6 and same_symbol(key_name, Layer.ODK, Layer.ODK_SHIFT): + if level == 6 and same_symbol(key.id, Layer.ODK, Layer.ODK_SHIFT): continue - key = svg.find(f".//g[@id=\"{SCAN_CODES['web'][key_name]}\"]", ns) - set_key_label(key, level, layout.layers[i][key_name]) + key_elem = svg.find(f".//g[@id=\"{key.web}\"]", ns) + set_key_label(key_elem, level, layout.layers[i][key.id]) return svg diff --git a/kalamine/generators/xkb.py b/kalamine/generators/xkb.py index 4ff88ac..a7823bf 100644 --- a/kalamine/generators/xkb.py +++ b/kalamine/generators/xkb.py @@ -12,24 +12,37 @@ if TYPE_CHECKING: from ..layout import KeyboardLayout +from ..key import KEYS, Hand, KeyCategory from ..template import load_tpl, substitute_lines -from ..utils import DK_INDEX, LAYER_KEYS, ODK_ID, DeadKeyDescr, SystemSymbol, hex_ord, load_data +from ..utils import DK_INDEX, ODK_ID, DeadKeyDescr, SystemSymbol, hex_ord, load_data XKB_KEY_SYM = load_data("key_sym") XKB_SPECIAL_KEYSYMS = { - SystemSymbol.Alt.value: "Alt_L", - SystemSymbol.AltGr.value: "ISO_Level3_Shift", - SystemSymbol.BackSpace.value: "BackSpace", - SystemSymbol.CapsLock.value: "Caps_Lock", - SystemSymbol.Compose.value: "Multi_key", - SystemSymbol.Control.value: "Control_L", - SystemSymbol.Escape.value: "Escape", - SystemSymbol.Return.value: "Return", - SystemSymbol.Shift.value: "Shift_L", + SystemSymbol.Alt.value: {Hand.Left: "Alt_L", Hand.Right: "Alt_R"}, + SystemSymbol.AltGr.value: {Hand.Left: "ISO_Level3_Shift"}, + SystemSymbol.BackSpace.value: {Hand.Left: "BackSpace"}, + SystemSymbol.CapsLock.value: {Hand.Left: "Caps_Lock"}, + SystemSymbol.Compose.value: {Hand.Left: "Multi_key"}, + SystemSymbol.Control.value: {Hand.Left: "Control_L", Hand.Right: "Control_R"}, + SystemSymbol.Escape.value: {Hand.Left: "Escape"}, + SystemSymbol.Return.value: {Hand.Left: "Return"}, + SystemSymbol.Shift.value: {Hand.Left: "Shift_L", Hand.Right: "Shift_R"}, } assert all(s.value in XKB_SPECIAL_KEYSYMS for s in SystemSymbol), \ tuple(s for s in SystemSymbol if s.value not in XKB_SPECIAL_KEYSYMS) -XKB_KEY_SYM.update(XKB_SPECIAL_KEYSYMS) + +def xkb_keysym(char: str, max_length: Optional[int] = None, hand: Optional[Hand]=None) -> str: + if char in XKB_SPECIAL_KEYSYMS: + keysyms = XKB_SPECIAL_KEYSYMS[char] + if hand in keysyms: + return keysyms[hand] + else: + return keysyms[Hand.Left] + elif char in XKB_KEY_SYM and (max_length is None or len(XKB_KEY_SYM[char]) <= max_length): + return XKB_KEY_SYM[char] + else: + return f"U{hex_ord(char).upper()}" + SPARE_KEYSYMS = ( "F20", @@ -71,7 +84,13 @@ def xkb_make_custom_dead_keys_keysyms(layout: "KeyboardLayout") -> XKB_Custom_Ke for s in layoutSymbols.strings: if len(s) >= 2: # Try to use one of the characters of the string - if candidates := tuple((cʹ, keysym) for c in s for cʹ in (c.lower(), c.upper()) if len(cʹ) == 1 and cʹ not in forbiden and (keysym := XKB_KEY_SYM.get(cʹ))): + candidates = tuple( + (cʹ, keysym) + for c in s + for cʹ in (c.lower(), c.upper()) + if len(cʹ) == 1 and cʹ not in forbiden and (keysym := XKB_KEY_SYM.get(cʹ)) + ) + if candidates: strings[s] = candidates[0][1] forbiden.add(candidates[0][0]) elif spares: @@ -93,13 +112,6 @@ def xkb_make_custom_dead_keys_keysyms(layout: "KeyboardLayout") -> XKB_Custom_Ke return XKB_Custom_Keysyms(strings, deadKeys) -def xkb_keysym(char: str, max_length: Optional[int] = None) -> str: - if char in XKB_KEY_SYM and (max_length is None or len(XKB_KEY_SYM[char]) <= max_length): - return XKB_KEY_SYM[char] - else: - return f"U{hex_ord(char).upper()}" - - def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, customDeadKeys: Optional[XKB_Custom_Keysyms]=None) -> List[str]: """GNU/Linux layout.""" @@ -115,18 +127,19 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, customDeadKeys: O customDeadKeys = XKB_Custom_Keysyms({}, {}) output: List[str] = [] - for key_name in LAYER_KEYS: - if key_name.startswith("-"): # separator + prev_category: Optional[KeyCategory] = None + for key in KEYS.values(): + if key.category is not prev_category: if output: output.append("") - output.append("//" + key_name[1:]) - continue + output.append("// " + key.category.description) + prev_category = key.category descs = [] symbols = [] for layer in layout.layers.values(): - if key_name in layer: - keysym = layer[key_name] + if key.id in layer: + keysym = layer[key.id] desc = keysym if keysymʹ := customDeadKeys.strings.get(keysym): symbol = keysymʹ @@ -140,10 +153,8 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, customDeadKeys: O desc = layout.dead_keys[keysym][keysym] symbol = odk_symbol if keysym == ODK_ID else f"dead_{name}" # regular key: use a keysym if possible, utf-8 otherwise - elif keysym in XKB_KEY_SYM and len(XKB_KEY_SYM[keysym]) <= max_length: - symbol = XKB_KEY_SYM[keysym] else: - symbol = f"U{hex_ord(keysym).upper()}" + symbol = xkb_keysym(keysym, max_length=max_length, hand=key.hand) else: desc = " " symbol = "VoidSymbol" @@ -152,12 +163,12 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, customDeadKeys: O symbols.append(symbol.ljust(max_length)) key_type = "" - key = "{{[ {0}, {1}, {2}, {3}]}}" # 4-level layout by default + key_template = "{{[ {0}, {1}, {2}, {3}]}}" # 4-level layout by default description = "{0} {1} {2} {3}" if all(s.startswith("VoidSymbol") for s in symbols): continue elif not symbols[0].startswith("VoidSymbol") and all(s == symbols[0] for s in symbols): - key = "{{{type}[{0}]}}" + key_template = "{{{type}[{0}]}}" description = "{0}" key_type = "ONE_LEVEL" symbols = [symbols[0]] @@ -167,10 +178,10 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, customDeadKeys: O if xkbcomp: # user-space XKB keymap file (standalone) # standalone XKB files work best with a dual-group solution: # one 4-level group for base+1dk, one two-level group for AltGr - key = "{{[ {}, {}, {}, {}],[ {}, {}]}}" + key_template = "{{[ {}, {}, {}, {}],[ {}, {}]}}" description = "{} {} {} {} {} {}" else: # eight_level XKB symbols (Neo-like) - key = "{{[ {0}, {1}, {4}, {5}, {2}, {3}]}}" + key_template = "{{[ {0}, {1}, {4}, {5}, {2}, {3}]}}" description = "{0} {1} {4} {5} {2} {3}" elif layout.has_altgr: del symbols[3] @@ -180,7 +191,7 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, customDeadKeys: O if key_type: key_type = f"""type[Group1] = "{key_type}", """ - line = f"key <{key_name.upper()}> {key.format(*symbols, type=key_type)};" + line = f"key <{key.xkb.upper()}> {key_template.format(*symbols, type=key_type)};" if show_description: line += (" // " + description.format(*descs)).rstrip() if line.endswith("\\"): @@ -238,7 +249,7 @@ def _compose_sequences(dk: DeadKeyDescr) -> Generator[Tuple[str, str], None, Non def xcompose(layout: "KeyboardLayout", customDeadKeys: XKB_Custom_Keysyms) -> Generator[str, None, None]: # Strings - for s, keysym in customDeadKeys.strings.items(): + for s, keysym in sorted(customDeadKeys.strings.items(), key=lambda x: x[1]): s = "".join(escapeString(s)) yield f"<{keysym}> : \"{s}\"" # Dead keys sequences diff --git a/kalamine/help.py b/kalamine/help.py index 38c0d39..80d8a84 100644 --- a/kalamine/help.py +++ b/kalamine/help.py @@ -1,8 +1,9 @@ from pathlib import Path from typing import Dict, List +from kalamine.key import KEYS + from .layout import KeyboardLayout -from .template import SCAN_CODES from .utils import Layer, load_data SEPARATOR = ( @@ -93,10 +94,10 @@ def dummy_layout( layout.geometry = geometry # ensure there is no empty keys (XXX maybe this should be in layout.py) - for key in SCAN_CODES["web"].keys(): - if key not in layout.layers[Layer.BASE].keys(): - layout.layers[Layer.BASE][key] = "\\" - layout.layers[Layer.SHIFT][key] = "|" + for key_ in KEYS.values(): + if key_.alphanum and key_.id not in layout.layers[Layer.BASE].keys(): + layout.layers[Layer.BASE][key_.id] = "\\" + layout.layers[Layer.SHIFT][key_.id] = "|" return layout diff --git a/kalamine/key.py b/kalamine/key.py new file mode 100644 index 0000000..6018531 --- /dev/null +++ b/kalamine/key.py @@ -0,0 +1,108 @@ +from dataclasses import dataclass +from enum import Enum, Flag, auto, unique +from typing import Any, Dict, Optional, Self + +from kalamine.utils import load_data + + +@unique +class Hand(Enum): + Left = auto() + Right = auto() + + @classmethod + def parse(cls, raw: str) -> Self: + for h in cls: + if h.name.casefold() == raw.casefold(): + return h + else: + raise ValueError(f"Cannot parse hand: “{raw}”") + + +@unique +class KeyCategory(Flag): + Digits = auto() + Letters1 = auto() + Letters2 = auto() + Letters3 = auto() + PinkyKeys = auto() + SpaceBar = auto() + System = auto() + Modifiers = auto() + InputMethod = auto() + Miscellaneous = auto() + AlphaNum = Digits | Letters1 | Letters2 | Letters3 | PinkyKeys | SpaceBar + + @classmethod + def parse(cls, raw: str) -> Self: + for kc in cls: + if kc.name.casefold() == raw.casefold(): + return kc + else: + raise ValueError(f"Cannot parse key category: “{raw}”") + + @property + def description(self) -> str: + match self: + case KeyCategory.Digits: + return "Digits" + case KeyCategory.Letters1: + return "Letters, first row" + case KeyCategory.Letters2: + return "Letters, second row" + case KeyCategory.Letters3: + return "Letters, third row" + case KeyCategory.PinkyKeys: + return "Pinky keys" + case KeyCategory.SpaceBar: + return "Space bar" + case KeyCategory.System: + return "System" + case KeyCategory.Modifiers: + return "Modifiers" + case KeyCategory.InputMethod: + return "Input method" + case KeyCategory.Miscellaneous: + return "Miscellaneous" + case _: + raise ValueError(f"No description ofr KeyCategory: {self}") + + +@dataclass +class Key: + xkb: str + web: Optional[str] = None + windows: Optional[str] = None + macos: Optional[str] = None + hand: Optional[Hand] = None + category: KeyCategory = KeyCategory.Miscellaneous + "Usual hand on standard (ISO, etc.) keyboard" + + @classmethod + def load_data(cls, data: Dict[str, Any]) -> Dict[str, Self]: + return { + key.xkb: key + for category, keys in data.items() + for key in (cls.parse(category=category, **entry) for entry in keys) + } + + @classmethod + def parse(cls, category: str, xkb: str, web: Optional[str], windows: Optional[str], macos: Optional[str], hand: Optional[str]) -> Self: + return cls( + category=KeyCategory.parse(category), + xkb=xkb, + web=web, + windows=windows, + macos=macos, + hand=Hand.parse(hand) if hand else None + ) + + @property + def id(self) -> str: + return self.xkb + + @property + def alphanum(self) -> bool: + return bool(self.category & KeyCategory.AlphaNum) + +KEYS = Key.load_data(load_data("scan_codes")) diff --git a/kalamine/layout.py b/kalamine/layout.py index 239f6f4..fff9e33 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -9,10 +9,10 @@ import tomli import yaml +from .key import KEYS from .utils import ( DEAD_KEYS, DK_INDEX, - LAYER_KEYS, ODK_ID, DeadKeyDescr, Layer, @@ -228,13 +228,13 @@ def __init__( # Fill special symbols special_symbols = frozenset(s.value for s in SystemSymbol) - for key in LAYER_KEYS: - if base_symbol := self.layers[Layer.BASE].get(key): + for key in KEYS.values(): + if base_symbol := self.layers[Layer.BASE].get(key.id): if base_symbol not in special_symbols: continue for keys in self.layers.values(): - if key not in keys: - keys[key] = base_symbol + if key.id not in keys: + keys[key.id] = base_symbol self._make_dead_keys(spc) @@ -281,14 +281,14 @@ def _parse_mapping(self, mapping: Dict[str, str | Dict[str, str]]): layer: Layer | None for raw_key, levels in mapping.items(): # TODO: parse key in various ways (XKB, Linux keycode) - if raw_key not in LAYER_KEYS: + if raw_key not in KEYS: raise ValueError(f"Unknown key: “{raw_key}”") key = raw_key # Check for key clone if isinstance(levels, str): # Check for clone if levels.startswith("(") and levels.endswith(")"): - if (clone := levels[1:-1]) and clone in LAYER_KEYS: + if (clone := levels[1:-1]) and clone in KEYS: for layer, keys in self.layers.items(): if value := keys.get(clone): self.layers[layer][key] = value @@ -332,9 +332,7 @@ def layout_has_char(char: str) -> bool: if id == ODK_ID: self.has_1dk = True - for key_name in LAYER_KEYS: - if key_name.startswith("-"): - continue + for key_name in KEYS: for layer in [Layer.ODK_SHIFT, Layer.ODK]: if key_name in self.layers[layer]: deadkey[self.layers[layer.necromance()][key_name]] = ( diff --git a/kalamine/template.py b/kalamine/template.py index bdf786e..87e8eec 100644 --- a/kalamine/template.py +++ b/kalamine/template.py @@ -9,9 +9,6 @@ from .layout import KeyboardLayout -SCAN_CODES = load_data("scan_codes") - - def substitute_lines(text: str, variable: str, lines: List[str]) -> str: prefix = "KALAMINE::" exp = re.compile(".*" + prefix + variable + ".*") diff --git a/kalamine/utils.py b/kalamine/utils.py index 38c6ce1..bb178ad 100644 --- a/kalamine/utils.py +++ b/kalamine/utils.py @@ -150,8 +150,6 @@ def parse_dead_key(raw: str) -> Optional[str]: for dk in DEAD_KEYS: DK_INDEX[dk.char] = dk -SCAN_CODES = load_data("scan_codes") - ODK_ID = "**" # must match the value in dead_keys.yaml @unique @@ -199,77 +197,3 @@ def prettify(cls, raw: str) -> str: return s.value.pretty else: return raw - -LAYER_KEYS = [ - "- Digits", - "ae01", - "ae02", - "ae03", - "ae04", - "ae05", - "ae06", - "ae07", - "ae08", - "ae09", - "ae10", - "- Letters, first row", - "ad01", - "ad02", - "ad03", - "ad04", - "ad05", - "ad06", - "ad07", - "ad08", - "ad09", - "ad10", - "- Letters, second row", - "ac01", - "ac02", - "ac03", - "ac04", - "ac05", - "ac06", - "ac07", - "ac08", - "ac09", - "ac10", - "- Letters, third row", - "ab01", - "ab02", - "ab03", - "ab04", - "ab05", - "ab06", - "ab07", - "ab08", - "ab09", - "ab10", - "- Pinky keys", - "ae11", - "ae12", - "ae13", - "ad11", - "ad12", - "ac11", - "ab11", - "tlde", - "bksl", - "lsgt", - "- Space bar", - "spce", - "menu", - "lfsh", - "rtsh", - "lalt", - "ralt", - "lctl", - "rctl", - "muhe", - "henk", - "- Miscellaneous", - "i172", - "rtrn", - "bksp", - "esc", -] From c7901bee2b0a154a8ee928bf899d285b33b90b53 Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Sun, 7 Apr 2024 14:30:51 +0200 Subject: [PATCH 06/17] XKB: Fix compose ordering --- kalamine/generators/xkb.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/kalamine/generators/xkb.py b/kalamine/generators/xkb.py index a7823bf..812230f 100644 --- a/kalamine/generators/xkb.py +++ b/kalamine/generators/xkb.py @@ -5,7 +5,9 @@ """ from dataclasses import dataclass +import functools import itertools +import locale from pathlib import Path from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Tuple @@ -75,13 +77,25 @@ class XKB_Output: compose: str +def _collate(s1: str, s2: str) -> int: + s1ʹ = s1.casefold() + s2ʹ = s2.casefold() + if s1ʹ < s2ʹ: + return -1 + elif s1ʹ > s2ʹ: + return +1 + else: + # Note: inverted on purpose, to get lower case before upper case + return locale.strcoll(s2, s1) + + def xkb_make_custom_dead_keys_keysyms(layout: "KeyboardLayout") -> XKB_Custom_Keysyms: layoutSymbols = layout.symbols forbiden = set(itertools.chain(layoutSymbols.strings, layoutSymbols.deadKeys)) spares = list(SPARE_KEYSYMS) strings = {} deadKeys = {} - for s in layoutSymbols.strings: + for s in sorted(layoutSymbols.strings, key=functools.cmp_to_key(_collate)): if len(s) >= 2: # Try to use one of the characters of the string candidates = tuple( @@ -97,7 +111,7 @@ def xkb_make_custom_dead_keys_keysyms(layout: "KeyboardLayout") -> XKB_Custom_Ke strings[s] = spares.pop(0) else: raise ValueError(f"Cannot encode string: “{s}”") - for c in layoutSymbols.deadKeys: + for c in sorted(layoutSymbols.deadKeys, key=functools.cmp_to_key(_collate)): dk = f"*{c}" if dk in DK_INDEX: continue From 145e6aa66b8194b7eb04ba8ca2241251bf9ef31a Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Sun, 7 Apr 2024 15:48:00 +0200 Subject: [PATCH 07/17] Fixes --- kalamine/data/scan_codes.yaml | 4 ++-- kalamine/generators/keylayout.py | 3 +++ kalamine/generators/web.py | 12 ++++-------- kalamine/generators/xkb.py | 27 +++++++++++++++------------ kalamine/key.py | 6 +++--- kalamine/layout.py | 6 +++--- kalamine/utils.py | 12 ++++++------ 7 files changed, 36 insertions(+), 34 deletions(-) diff --git a/kalamine/data/scan_codes.yaml b/kalamine/data/scan_codes.yaml index bda3f00..7a5cdae 100644 --- a/kalamine/data/scan_codes.yaml +++ b/kalamine/data/scan_codes.yaml @@ -325,8 +325,8 @@ InputMethod: - xkb: "henk" web: "Convert" windows: null - macos: Right - hand: null + macos: null + hand: Right # Miscellaneous Miscellaneous: - xkb: "i172" diff --git a/kalamine/generators/keylayout.py b/kalamine/generators/keylayout.py index 5566501..2c832e1 100644 --- a/kalamine/generators/keylayout.py +++ b/kalamine/generators/keylayout.py @@ -121,6 +121,9 @@ def append_actions(id: str, symbol: str, actions: List[Tuple[str, str]]) -> None # normal key actions prev_category: Optional[KeyCategory] = None for key in KEYS.values(): + if key.macos is None: + continue + if key.category is not prev_category: ret_actions.append("") ret_actions.append(f"") diff --git a/kalamine/generators/web.py b/kalamine/generators/web.py index 4fd1ab8..f3fad6c 100644 --- a/kalamine/generators/web.py +++ b/kalamine/generators/web.py @@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, Dict, List, Optional from xml.etree import ElementTree as ET - if TYPE_CHECKING: from ..layout import KeyboardLayout @@ -82,9 +81,8 @@ def set_key_label(key: Optional[ET.Element], lvl: int, char: str) -> None: def same_symbol(key_name: str, lower: Layer, upper: Layer): up = layout.layers[upper] low = layout.layers[lower] - if key_name not in up or key_name not in low: - return False - return up[key_name] == pretty_upper_key(low[key_name], blank_if_obvious=False) + return key_name in up and key_name in low and \ + up[key_name] == pretty_upper_key(low[key_name], blank_if_obvious=False) # Parse the SVG template # res = pkgutil.get_data(__package__, "templates/x-keyboard.svg") @@ -98,16 +96,14 @@ def same_symbol(key_name: str, lower: Layer, upper: Layer): # TODO: warning continue - level = 0 - for i in [ + for level, i in enumerate(( Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT, Layer.ODK, Layer.ODK_SHIFT, - ]: - level += 1 + ), start=1): if key.id not in layout.layers[i]: continue if level == 1 and same_symbol(key.id, Layer.BASE, Layer.SHIFT): diff --git a/kalamine/generators/xkb.py b/kalamine/generators/xkb.py index 812230f..22d7e3f 100644 --- a/kalamine/generators/xkb.py +++ b/kalamine/generators/xkb.py @@ -99,7 +99,7 @@ def xkb_make_custom_dead_keys_keysyms(layout: "KeyboardLayout") -> XKB_Custom_Ke if len(s) >= 2: # Try to use one of the characters of the string candidates = tuple( - (cʹ, keysym) + (cʹ, keysym) for c in s for cʹ in (c.lower(), c.upper()) if len(cʹ) == 1 and cʹ not in forbiden and (keysym := XKB_KEY_SYM.get(cʹ)) @@ -143,14 +143,9 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, customDeadKeys: O output: List[str] = [] prev_category: Optional[KeyCategory] = None for key in KEYS.values(): - if key.category is not prev_category: - if output: - output.append("") - output.append("// " + key.category.description) - prev_category = key.category - descs = [] symbols = [] + defined = False for layer in layout.layers.values(): if key.id in layer: keysym = layer[key.id] @@ -169,6 +164,7 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, customDeadKeys: O # regular key: use a keysym if possible, utf-8 otherwise else: symbol = xkb_keysym(keysym, max_length=max_length, hand=key.hand) + defined = True else: desc = " " symbol = "VoidSymbol" @@ -176,12 +172,19 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, customDeadKeys: O descs.append(desc) symbols.append(symbol.ljust(max_length)) + if not symbols or not defined: + continue + + if key.category is not prev_category: + if output: + output.append("") + output.append("// " + key.category.description) + prev_category = key.category + key_type = "" key_template = "{{[ {0}, {1}, {2}, {3}]}}" # 4-level layout by default description = "{0} {1} {2} {3}" - if all(s.startswith("VoidSymbol") for s in symbols): - continue - elif not symbols[0].startswith("VoidSymbol") and all(s == symbols[0] for s in symbols): + if not symbols[0].startswith("VoidSymbol") and all(s == symbols[0] for s in symbols): key_template = "{{{type}[{0}]}}" description = "{0}" key_type = "ONE_LEVEL" @@ -237,7 +240,7 @@ def xkb_symbols(layout: "KeyboardLayout") -> XKB_Output: symbols = substitute_lines(symbols, "LAYOUT", xkb_table(layout, xkbcomp=False, customDeadKeys=customDeadKeysKeysyms)) compose = "\n".join(xcompose(layout, customDeadKeysKeysyms)) if customDeadKeysKeysyms else "" - + return XKB_Output(symbols.replace("//#", "//"), compose) @@ -298,7 +301,7 @@ def xcompose(layout: "KeyboardLayout", customDeadKeys: XKB_Custom_Keysyms) -> Ge def xkb_write_files(path: Path, result: XKB_Output): with path.open("w", encoding="utf-8", newline="\n") as file: file.write(result.symbols) - if result.compose is None: + if not result.compose: return path = path.with_suffix(".xkb_compose") with path.open("w", encoding="utf-8", newline="\n") as file: diff --git a/kalamine/key.py b/kalamine/key.py index 6018531..a11214f 100644 --- a/kalamine/key.py +++ b/kalamine/key.py @@ -40,7 +40,7 @@ def parse(cls, raw: str) -> Self: return kc else: raise ValueError(f"Cannot parse key category: “{raw}”") - + @property def description(self) -> str: match self: @@ -85,7 +85,7 @@ def load_data(cls, data: Dict[str, Any]) -> Dict[str, Self]: for category, keys in data.items() for key in (cls.parse(category=category, **entry) for entry in keys) } - + @classmethod def parse(cls, category: str, xkb: str, web: Optional[str], windows: Optional[str], macos: Optional[str], hand: Optional[str]) -> Self: return cls( @@ -100,7 +100,7 @@ def parse(cls, category: str, xkb: str, web: Optional[str], windows: Optional[st @property def id(self) -> str: return self.xkb - + @property def alphanum(self) -> bool: return bool(self.category & KeyCategory.AlphaNum) diff --git a/kalamine/layout.py b/kalamine/layout.py index fff9e33..884174f 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -224,7 +224,7 @@ def __init__( # Extra mapping if mapping := layout_data.get("mapping"): - self._parse_mapping(mapping) + self._parse_extra_mapping(mapping) # Fill special symbols special_symbols = frozenset(s.value for s in SystemSymbol) @@ -277,7 +277,7 @@ def _parse_dead_keys(self, raw: Dict[str, Any]) -> Dict[str, DeadKeyDescr]: def _parse_value(self, raw: str, strip=False) -> str: return SpecialSymbol.parse(raw.strip() if strip else raw) - def _parse_mapping(self, mapping: Dict[str, str | Dict[str, str]]): + def _parse_extra_mapping(self, mapping: Dict[str, str | Dict[str, str]]): layer: Layer | None for raw_key, levels in mapping.items(): # TODO: parse key in various ways (XKB, Linux keycode) @@ -461,7 +461,7 @@ def _get_geometry(self, layers: Optional[List[Layer]] = None) -> List[str]: for i in layers: template = self._fill_template(template, rows, i) return template - + @property def geometry(self) -> str: """ANSI, ISO, ERGO.""" diff --git a/kalamine/utils.py b/kalamine/utils.py index bb178ad..5c08ea8 100644 --- a/kalamine/utils.py +++ b/kalamine/utils.py @@ -85,7 +85,7 @@ def necromance(self) -> "Layer": def upper_key(letter: Optional[str]) -> Optional[str]: if not letter: return None - + special_symbols = {s.value for s in SystemSymbol} if letter in special_symbols: return letter @@ -108,7 +108,7 @@ def upper_key(letter: Optional[str]) -> Optional[str]: if len(letter) == 1 and letter.upper() != letter.lower(): return letter.upper() - + return None @@ -117,10 +117,10 @@ def pretty_upper_key(letter: Optional[str], blank_if_obvious: bool = True) -> st becomes blank if it's an obvious uppercase version of the base character.""" if (letterʹ := upper_key(letter)) is None: - return " " + return "" if blank_if_obvious else (letter or "") # dead key or non-letter character - return " " if blank_if_obvious else letterʹ + return "" if blank_if_obvious else letterʹ @dataclass @@ -135,7 +135,7 @@ class DeadKeyDescr: @staticmethod def is_dead_key(raw: str) -> bool: return len(raw) == 2 and raw[0] == "*" - + @staticmethod def parse_dead_key(raw: str) -> Optional[str]: if len(raw) == 2 and raw[0] == "*": @@ -189,7 +189,7 @@ def parse(cls, raw: str) -> str: return s.value.value else: return raw - + @classmethod def prettify(cls, raw: str) -> str: for s in cls: From 10eb14927ee1588e4612ee2bdbdac96b50bfb200 Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Sun, 7 Apr 2024 16:34:21 +0200 Subject: [PATCH 08/17] TMx central column --- kalamine/data/geometry.yaml | 50 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/kalamine/data/geometry.yaml b/kalamine/data/geometry.yaml index ad9da63..6d3f6d1 100644 --- a/kalamine/data/geometry.yaml +++ b/kalamine/data/geometry.yaml @@ -165,35 +165,35 @@ ERGO: TYPEMATRIX: template: | - ╭╌╌╌╌╌┰─────┬─────┬─────┬─────┬─────┰─────┬─────┬─────┬─────┬─────┰╌╌╌╌╌┬╌╌╌╌╌╮ - ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ - ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ - ├╌╌╌╌╌╂─────┼─────┼─────┼─────┼─────╂─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┼╌╌╌╌╌┤ - ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ - ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ - ├╌╌╌╌╌╂─────┼─────┼─────┼─────┼─────╂─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┼╌╌╌╌╌┤ - ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ - ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ - ┆ ┠─────┼─────┼─────┼─────┼─────╂─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┤ ┆ - ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ - ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ - ├╌╌╌╌╌╂─────┼─────┼─────┼─────┴─────┸─────┴─────┼─────┼─────┴─────┸╌╌╌╌╌┴╌╌╌╌╌╯ - ┆ ┆ ┆ ┆ ┆ ┆ ┆ - ┆ ┆ ┆ ┆ ┆ ┆ ┆ - ╰╌╌╌╌╌┴╌╌╌╌╌┴╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┤ - ┆ ┆ ┆ ┆ - ┆ ┆ ┆ ┆ - ╰╌╌╌╌╌╯ ╰╌╌╌╌╌╯ + ╭╌╌╌╌╌┰─────┬─────┬─────┬─────┬─────┰─────┰─────┬─────┬─────┬─────┬─────┰╌╌╌╌╌┬╌╌╌╌╌╮ + ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ + ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ + ├╌╌╌╌╌╂─────┼─────┼─────┼─────┼─────┨ ┠─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┼╌╌╌╌╌┤ + ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ + ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ + ├╌╌╌╌╌╂─────┼─────┼─────┼─────┼─────╂─────╂─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┼╌╌╌╌╌┤ + ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ + ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ + ┆ ┠─────┼─────┼─────┼─────┼─────┨ ┠─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┤ ┆ + ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ + ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ + ├╌╌╌╌╌╂─────┼─────┼─────┼─────┴─────┸─────┸─────┴─────┼─────┼─────┴─────┸╌╌╌╌╌┴╌╌╌╌╌╯ + ┆ ┆ ┆ ┆ ┆ ┆ ┆ + ┆ ┆ ┆ ┆ ┆ ┆ ┆ + ╰╌╌╌╌╌┴╌╌╌╌╌┴╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┤ + ┆ ┆ ┆ ┆ + ┆ ┆ ┆ ┆ + ╰╌╌╌╌╌╯ ╰╌╌╌╌╌╯ rows: - offset: 2 - keys: [ tlde, ae01, ae02, ae03, ae04, ae05, ae06, ae07, ae08, ae09, ae10, ae11, ae12 ] + keys: [ tlde, ae01, ae02, ae03, ae04, ae05, xxxx, ae06, ae07, ae08, ae09, ae10, ae11, ae12 ] - offset: 2 - keys: [ tab, ad01, ad02, ad03, ad04, ad05, ad06, ad07, ad08, ad09, ad10, ad11, ad12 ] + keys: [ tab, ad01, ad02, ad03, ad04, ad05, bksp, ad06, ad07, ad08, ad09, ad10, ad11, ad12 ] - offset: 2 - keys: [ lfsh, ac01, ac02, ac03, ac04, ac05, ac06, ac07, ac08, ac09, ac10, ac11, rtsh ] + keys: [ lfsh, ac01, ac02, ac03, ac04, ac05, xxxx, ac06, ac07, ac08, ac09, ac10, ac11, rtsh ] - offset: 2 - keys: [ lfsh, ab01, ab02, ab03, ab04, ab05, ab06, ab07, ab08, ab09, ab10, bksl, rtsh ] + keys: [ lfsh, ab01, ab02, ab03, ab04, ab05, rtrn, ab06, ab07, ab08, ab09, ab10, bksl, rtsh ] - offset: 2 - keys: [ lctl, lsgt, menu, muhe, spce, xxxx, xxxx, spce, henk ] + keys: [ lctl, lsgt, menu, muhe, spce, xxxx, xxxx, xxxx, spce, henk ] - offset: 20 - keys: [ lalt, xxxx, xxxx, xxxx, xxxx, ralt ] + keys: [ lalt, xxxx, xxxx, xxxx, xxxx, xxxx, ralt ] From f03997d9534c2ecc8f582a9135ef8e635012dc74 Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Sun, 7 Apr 2024 19:54:10 +0200 Subject: [PATCH 09/17] Fixes --- kalamine/data/geometry.yaml | 58 +++++++------- kalamine/data/scan_codes.yaml | 143 +++++++++++++++++++++++++++++++++- kalamine/generators/xkb.py | 44 +++++++---- kalamine/layout.py | 66 +++++++++------- kalamine/utils.py | 18 +++++ 5 files changed, 259 insertions(+), 70 deletions(-) diff --git a/kalamine/data/geometry.yaml b/kalamine/data/geometry.yaml index 6d3f6d1..8cdcbe2 100644 --- a/kalamine/data/geometry.yaml +++ b/kalamine/data/geometry.yaml @@ -165,35 +165,41 @@ ERGO: TYPEMATRIX: template: | - ╭╌╌╌╌╌┰─────┬─────┬─────┬─────┬─────┰─────┰─────┬─────┬─────┬─────┬─────┰╌╌╌╌╌┬╌╌╌╌╌╮ - ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ - ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ - ├╌╌╌╌╌╂─────┼─────┼─────┼─────┼─────┨ ┠─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┼╌╌╌╌╌┤ - ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ - ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ - ├╌╌╌╌╌╂─────┼─────┼─────┼─────┼─────╂─────╂─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┼╌╌╌╌╌┤ - ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ - ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ - ┆ ┠─────┼─────┼─────┼─────┼─────┨ ┠─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┤ ┆ - ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ - ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ - ├╌╌╌╌╌╂─────┼─────┼─────┼─────┴─────┸─────┸─────┴─────┼─────┼─────┴─────┸╌╌╌╌╌┴╌╌╌╌╌╯ - ┆ ┆ ┆ ┆ ┆ ┆ ┆ - ┆ ┆ ┆ ┆ ┆ ┆ ┆ - ╰╌╌╌╌╌┴╌╌╌╌╌┴╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┤ - ┆ ┆ ┆ ┆ - ┆ ┆ ┆ ┆ - ╰╌╌╌╌╌╯ ╰╌╌╌╌╌╯ + ╭╌╌╌╌╌┰─────┬─────┬─────┬─────┬─────┰─────┰─────┬─────┬─────┬─────┬─────┰╌╌╌╌╌┬╌╌╌╌╌┬╌╌╌╌╌╮ + ┆ ⎋ ┆ ┆ ┆ ┆ ┆ ┆ ⌦ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ Num ┆ + ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + ├╌╌╌╌╌╁─────┼─────┼─────┼─────┼─────╁─────╁─────┼─────┼─────┼─────┼─────╁╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌┤ + ┆ ┃ │ │ │ │ ┃ ⌫ ┃ │ │ │ │ ┃ ┆ ┆ ┆ + ┆ ┃ 1 │ 2 │ 3 │ 4 │ 5 ┃ ┃ 6 │ 7 │ 8 │ 9 │ 0 ┃ ┆ ┆ ┆ + ├╌╌╌╌╌╂─────┼─────┼─────┼─────┼─────┨ ┠─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌┤ + ┆ ↹ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ ┆ + ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ ┆ + ├╌╌╌╌╌╂─────┼─────┼─────┼─────┼─────╂─────╂─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌┤ + ┆ ⇧ ┃ │ │ │ │ ┃ ⏎ ┃ │ │ │ │ ┃ ┆ ⇧ ┆ ⇬ ┆ + ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ ┆ + ┆ ┠─────┼─────┼─────┼─────┼─────┨ ┠─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┤ ├╌╌╌╌╌┤ + ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ ┆ + ┆ ┃ │ │ │ │ ┃ ┃ │ │ │ │ ┃ ┆ ┆ ┆ + ├╌╌╌╌╌╀─────┼─────┼─────┼─────┴─────┸─────┸─────┴─────┼─────┼─────┼─────╀╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌┤ + ┆ ⎈ ┆ ┆ ≣ ┆ ┆ ␣ ┆ ┆ ⇱ ┆ ⬆ ┆ ⇲ ┆ ⎈ ┆ ⎗ ┆ + ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + ├╌╌╌╌╌┼╌╌╌╌╌┴╌╌┬╌╌┴╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌┤ ├╌╌╌╌╌┤ + ┆ Fn ┆ ◆ ┆ ⎇ ┆ ┆ ⎇ ┆ ⬅ ┆ ⬇ ┆ ➡ ┆ ┆ ⎘ ┆ + ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + ╰╌╌╌╌╌┴╌╌╌╌╌╌╌╌┴╌╌╌╌╌╌╌╌╯ ╰╌╌╌╌╌┴╌╌╌╌╌┴╌╌╌╌╌┴╌╌╌╌╌┴╌╌╌╌╌╯╌╌╌╌╌╯ + digits_row: 1 rows: - offset: 2 - keys: [ tlde, ae01, ae02, ae03, ae04, ae05, xxxx, ae06, ae07, ae08, ae09, ae10, ae11, ae12 ] + keys: [ esc, fk01, fk02, fk03, fk04, fk05, dele, fk06, fk07, fk08, fk09, fk10, fk11, fk12 ] - offset: 2 - keys: [ tab, ad01, ad02, ad03, ad04, ad05, bksp, ad06, ad07, ad08, ad09, ad10, ad11, ad12 ] + keys: [ tlde, ae01, ae02, ae03, ae04, ae05, bksp, ae06, ae07, ae08, ae09, ae10, ae11, ae12, i148 ] - offset: 2 - keys: [ lfsh, ac01, ac02, ac03, ac04, ac05, xxxx, ac06, ac07, ac08, ac09, ac10, ac11, rtsh ] + keys: [ tab, ad01, ad02, ad03, ad04, ad05, xxxx, ad06, ad07, ad08, ad09, ad10, ad11, ad12, i163 ] - offset: 2 - keys: [ lfsh, ab01, ab02, ab03, ab04, ab05, rtrn, ab06, ab07, ab08, ab09, ab10, bksl, rtsh ] + keys: [ lfsh, ac01, ac02, ac03, ac04, ac05, rtrn, ac06, ac07, ac08, ac09, ac10, ac11, rtsh, caps ] - offset: 2 - keys: [ lctl, lsgt, menu, muhe, spce, xxxx, xxxx, xxxx, spce, henk ] - - offset: 20 - keys: [ lalt, xxxx, xxxx, xxxx, xxxx, xxxx, ralt ] + keys: [ xxxx, ab01, ab02, ab03, ab04, ab05, xxxx, ab06, ab07, ab08, ab09, ab10, bksl, xxxx, i180 ] + - offset: 2 + keys: [ lctl, lsgt, menu, muhe, spce, xxxx, spce, xxxx, spce, henk, home, up, end, rctl, pgup ] + - offset: 8 + keys: [ lwin, xxxx, lalt, xxxx, xxxx, xxxx, xxxx, xxxx, ralt, left, down, rght, xxxx, pgdn ] diff --git a/kalamine/data/scan_codes.yaml b/kalamine/data/scan_codes.yaml index 7a5cdae..b19ab1c 100644 --- a/kalamine/data/scan_codes.yaml +++ b/kalamine/data/scan_codes.yaml @@ -265,11 +265,11 @@ SpaceBar: macos: "49" hand: null System: -- xkb: "menu" +- xkb: "tab" web: null windows: null macos: null - hand: null + hand: Left - xkb: "rtrn" web: null windows: null @@ -280,11 +280,126 @@ System: windows: null macos: null hand: null +- xkb: "dele" + web: null + windows: null + macos: null + hand: null - xkb: "esc" web: null windows: null macos: null hand: null +- xkb: "menu" + web: null + windows: null + macos: null + hand: null +- xkb: "home" + web: null + windows: null + macos: null + hand: null +- xkb: "up" + web: null + windows: null + macos: null + hand: null +- xkb: "end" + web: null + windows: null + macos: null + hand: null +- xkb: "pgup" + web: null + windows: null + macos: null + hand: null +- xkb: "left" + web: null + windows: null + macos: null + hand: null +- xkb: "down" + web: null + windows: null + macos: null + hand: null +- xkb: "rght" + web: null + windows: null + macos: null + hand: null +- xkb: "pgdn" + web: null + windows: null + macos: null + hand: null +- xkb: "fk01" + web: null + windows: null + macos: null + hand: null +- xkb: "fk02" + web: null + windows: null + macos: null + hand: null +- xkb: "fk03" + web: null + windows: null + macos: null + hand: null +- xkb: "fk04" + web: null + windows: null + macos: null + hand: null +- xkb: "fk05" + web: null + windows: null + macos: null + hand: null +- xkb: "dele" + web: null + windows: null + macos: null + hand: null +- xkb: "fk06" + web: null + windows: null + macos: null + hand: null +- xkb: "fk07" + web: null + windows: null + macos: null + hand: null +- xkb: "fk08" + web: null + windows: null + macos: null + hand: null +- xkb: "fk09" + web: null + windows: null + macos: null + hand: null +- xkb: "fk10" + web: null + windows: null + macos: null + hand: null +- xkb: "fk11" + web: null + windows: null + macos: null + hand: null +- xkb: "fk12" + web: null + windows: null + macos: null + hand: null Modifiers: - xkb: "lfsh" web: null @@ -296,6 +411,11 @@ Modifiers: windows: null macos: null hand: Right +- xkb: "caps" + web: null + windows: null + macos: null + hand: Left - xkb: "lalt" web: "AltLeft" windows: null @@ -329,8 +449,23 @@ InputMethod: hand: Right # Miscellaneous Miscellaneous: -- xkb: "i172" +- xkb: "i148" # Calc + web: null + windows: null + macos: null + hand: null +- xkb: "i163" # Mail + web: null + windows: null + macos: null + hand: null +- xkb: "i172" # Play/Pause web: null windows: null macos: null - hand: null \ No newline at end of file + hand: null +- xkb: "i180" # Home page + web: null + windows: null + macos: null + hand: null diff --git a/kalamine/generators/xkb.py b/kalamine/generators/xkb.py index 22d7e3f..8008ab2 100644 --- a/kalamine/generators/xkb.py +++ b/kalamine/generators/xkb.py @@ -26,9 +26,23 @@ SystemSymbol.CapsLock.value: {Hand.Left: "Caps_Lock"}, SystemSymbol.Compose.value: {Hand.Left: "Multi_key"}, SystemSymbol.Control.value: {Hand.Left: "Control_L", Hand.Right: "Control_R"}, + SystemSymbol.Delete.value: {Hand.Left: "Delete"}, SystemSymbol.Escape.value: {Hand.Left: "Escape"}, SystemSymbol.Return.value: {Hand.Left: "Return"}, SystemSymbol.Shift.value: {Hand.Left: "Shift_L", Hand.Right: "Shift_R"}, + SystemSymbol.Super.value: {Hand.Left: "Super_L", Hand.Right: "Super_R"}, + SystemSymbol.Tab.value: {Hand.Left: "Tab"}, + SystemSymbol.RightTab.value: {Hand.Left: "Tab"}, + SystemSymbol.LeftTab.value: {Hand.Left: "ISO_Left_Tab"}, + SystemSymbol.ArrowUp.value: {Hand.Left: "Up"}, + SystemSymbol.ArrowDown.value: {Hand.Left: "Down"}, + SystemSymbol.ArrowLeft.value: {Hand.Left: "Left"}, + SystemSymbol.ArrowRight.value: {Hand.Left: "Right"}, + SystemSymbol.PageUp.value: {Hand.Left: "Prior"}, + SystemSymbol.PageDown.value: {Hand.Left: "Next"}, + SystemSymbol.Home.value: {Hand.Left: "Home"}, + SystemSymbol.End.value: {Hand.Left: "End"}, + SystemSymbol.Menu.value: {Hand.Left: "Menu"}, } assert all(s.value in XKB_SPECIAL_KEYSYMS for s in SystemSymbol), \ tuple(s for s in SystemSymbol if s.value not in XKB_SPECIAL_KEYSYMS) @@ -146,9 +160,12 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, customDeadKeys: O descs = [] symbols = [] defined = False - for layer in layout.layers.values(): - if key.id in layer: - keysym = layer[key.id] + for layer, keys in layout.layers.items(): + if key.id in keys: + keysym = keys[key.id] + # Hack for Tab + if layer.value % 2 == 1 and keysym in ("↹", "⇥"): + keysym = "⇤" desc = keysym if keysymʹ := customDeadKeys.strings.get(keysym): symbol = keysymʹ @@ -181,15 +198,18 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, customDeadKeys: O output.append("// " + key.category.description) prev_category = key.category - key_type = "" key_template = "{{[ {0}, {1}, {2}, {3}]}}" # 4-level layout by default description = "{0} {1} {2} {3}" - if not symbols[0].startswith("VoidSymbol") and all(s == symbols[0] for s in symbols): - key_template = "{{{type}[{0}]}}" + if all(s == symbols[0] for s in symbols): + key_template = """{{type[Group1] = \"ONE_LEVEL\",\n [ {0} ]}}""" description = "{0}" - key_type = "ONE_LEVEL" - symbols = [symbols[0]] - descs = [descs[0]] + symbols = symbols[0:1] + descs = descs[0:1] + elif all((i % 2 == 0 and s == symbols[0]) or (i % 2 == 1 and s == symbols[1]) for i, s in enumerate(symbols)): + key_template = """{{type[Group1] = \"TWO_LEVEL\",\n [ {0}, {1} ]}}""" + description = "{0} {1}" + symbols = symbols[:2] + descs = descs[:2] elif layout.has_altgr and layout.has_1dk: # 6 layers are needed: they won't fit on the 4-level format. if xkbcomp: # user-space XKB keymap file (standalone) @@ -206,9 +226,8 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False, customDeadKeys: O del descs[3] del descs[2] - if key_type: - key_type = f"""type[Group1] = "{key_type}", """ - line = f"key <{key.xkb.upper()}> {key_template.format(*symbols, type=key_type)};" + keycode = f"<{key.xkb.upper()}>" + line = f"key {keycode: <6} {key_template.format(*symbols)};" if show_description: line += (" // " + description.format(*descs)).rstrip() if line.endswith("\\"): @@ -287,7 +306,6 @@ def xcompose(layout: "KeyboardLayout", customDeadKeys: XKB_Custom_Keysyms) -> Ge if predefined.get(base) == result: # Skip predefined sequence continue - print(dk.name, base, result) # TODO: general chained dead keys? if base == dk.char: base_keysym = dk_keysym diff --git a/kalamine/layout.py b/kalamine/layout.py index 884174f..0f7eb1c 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -115,6 +115,7 @@ class RowDescr: class GeometryDescr: template: str rows: List[RowDescr] + digit_row: int = 0 @classmethod def from_dict(cls: Type[T], src: Dict) -> T: @@ -134,6 +135,10 @@ class LayoutSymbols: deadKeys: Set[str] +TEMPLATE_DUMMY_KEY = "xxxx" +TEMPLATE_KEY_WIDTH = 6 + + ### # Main # @@ -181,7 +186,7 @@ def __init__( # Angle Mod permutation if angle_mod: - last_row = rows[3] + last_row = rows[GEOMETRY[self.meta["geometry"]].digit_row + 3] if last_row.keys[0] == "lsgt": # should bevome ['ab05', 'lsgt', 'ab01', 'ab02', 'ab03', 'ab04'] last_row.keys[:6] = [last_row.keys[5]] + last_row.keys[:5] @@ -227,14 +232,16 @@ def __init__( self._parse_extra_mapping(mapping) # Fill special symbols - special_symbols = frozenset(s.value for s in SystemSymbol) for key in KEYS.values(): if base_symbol := self.layers[Layer.BASE].get(key.id): - if base_symbol not in special_symbols: + if not SystemSymbol.is_system_symbol(base_symbol): continue - for keys in self.layers.values(): + shift_symbol = self.layers[Layer.SHIFT].get(key.id, "") + if not SystemSymbol.is_system_symbol(shift_symbol): + shift_symbol = base_symbol + for layer, keys in self.layers.items(): if key.id not in keys: - keys[key.id] = base_symbol + keys[key.id] = base_symbol if layer.value % 2 == 0 else shift_symbol self._make_dead_keys(spc) @@ -277,6 +284,13 @@ def _parse_dead_keys(self, raw: Dict[str, Any]) -> Dict[str, DeadKeyDescr]: def _parse_value(self, raw: str, strip=False) -> str: return SpecialSymbol.parse(raw.strip() if strip else raw) + @staticmethod + def _parse_key_ref(raw: str) -> Optional[str]: + if raw.startswith("(") and raw.endswith(")"): + if (clone := raw[1:-1]) and clone in KEYS: + return clone + return None + def _parse_extra_mapping(self, mapping: Dict[str, str | Dict[str, str]]): layer: Layer | None for raw_key, levels in mapping.items(): @@ -287,19 +301,21 @@ def _parse_extra_mapping(self, mapping: Dict[str, str | Dict[str, str]]): # Check for key clone if isinstance(levels, str): # Check for clone - if levels.startswith("(") and levels.endswith(")"): - if (clone := levels[1:-1]) and clone in KEYS: - for layer, keys in self.layers.items(): - if value := keys.get(clone): - self.layers[layer][key] = value - continue + if clone := self._parse_key_ref(levels): + for layer, keys in self.layers.items(): + if value := keys.get(clone): + self.layers[layer][key] = value + continue raise ValueError(f"Unsupported key mapping: {raw_key}: {levels}") for raw_layer, raw_value in levels.items(): - value = self._parse_value(raw_value) if (layer := Layer.parse(raw_layer)) is None: raise ValueError(f"Cannot parse layer: “{raw_layer}”") + if clone := self._parse_key_ref(raw_value): + if (value := self.layers[layer].get(clone)) is None: + continue else: - self.layers[layer][key] = value + value = self._parse_value(raw_value) + self.layers[layer][key] = value def _make_dead_keys(self, spc: Dict[str, str]) -> None: """Build a deadkey dict.""" @@ -355,16 +371,17 @@ def _parse_template( ) -> None: """Extract a keyboard layer from a template.""" - j = 0 col_offset = 0 if layer_number == Layer.BASE else 2 - for row in rows: - i = row.offset + col_offset + for j, row in enumerate(rows): keys = row.keys base = template[2 + j * 3] shift = template[1 + j * 3] - for key in keys: + for i, key in zip(itertools.count(row.offset + col_offset, TEMPLATE_KEY_WIDTH), keys): + if key == TEMPLATE_DUMMY_KEY: + continue + base_key: Optional[str] = self._parse_value(base[i-1:i+1], strip=True) shift_key: Optional[str] = self._parse_value(shift[i-1:i+1], strip=True) @@ -397,9 +414,6 @@ def _parse_template( else: raise ValueError(f"Undefined dead key: {shift_key}") - i += 6 - j += 1 - ### # Geometry: base, full, altgr # @@ -416,15 +430,16 @@ def _fill_template( col_offset = 2 shift_prevails = False - j = 0 - for row in rows: - i = row.offset + col_offset + for j, row in enumerate(rows): keys = row.keys base = list(template[2 + j * 3]) shift = list(template[1 + j * 3]) - for key in keys: + for i, key in zip(itertools.count(row.offset + col_offset, TEMPLATE_KEY_WIDTH), keys): + if key == TEMPLATE_DUMMY_KEY: + continue + indexes = slice(i - 1, i + 1) base_key = " " @@ -444,11 +459,8 @@ def _fill_template( if pretty_upper_key(base_key, blank_if_obvious=False) != shift_key: shift[indexes] = shift_key.rjust(2) - i += 6 - template[2 + j * 3] = "".join(base) template[1 + j * 3] = "".join(shift) - j += 1 return template diff --git a/kalamine/utils.py b/kalamine/utils.py index 5c08ea8..ab2d562 100644 --- a/kalamine/utils.py +++ b/kalamine/utils.py @@ -160,9 +160,23 @@ class SystemSymbol(Enum): CapsLock = "⇬" Compose = "⎄" Control = "⎈" + Delete = "⌦" Escape = "⎋" Return = "⏎" Shift = "⇧" + Super = "◆" + Tab = "↹" + RightTab = "⇥" + LeftTab = "⇤" + ArrowUp = "⬆" + ArrowDown = "⬇" + ArrowLeft = "⬅" + ArrowRight = "➡" + PageUp = "⎗" + PageDown = "⎘" + Home = "⇱" + End = "⇲" + Menu = "≣" @classmethod def parse(cls, raw: str) -> Optional[Self]: @@ -171,6 +185,10 @@ def parse(cls, raw: str) -> Optional[Self]: return s else: return None + + @classmethod + def is_system_symbol(cls, raw: str) -> bool: + return cls.parse(raw) is not None @dataclass class SpecialSymbolEntry: From e48d396fc8459ab000e5a0b20a279cd55585d2c2 Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Sun, 7 Apr 2024 20:09:18 +0200 Subject: [PATCH 10/17] Clear template --- kalamine/layout.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/kalamine/layout.py b/kalamine/layout.py index 0f7eb1c..5fdd82c 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -419,11 +419,13 @@ def _parse_template( # def _fill_template( - self, template: List[str], rows: List[RowDescr], layer_number: Layer + self, template: List[str], rows: List[RowDescr], layer_number: Optional[Layer] ) -> List[str]: """Fill a template with a keyboard layer.""" - if layer_number == Layer.BASE: + if layer_number is None: + col_offset = 0 + elif layer_number == Layer.BASE: col_offset = 0 shift_prevails = True else: # AltGr or 1dk @@ -440,6 +442,12 @@ def _fill_template( if key == TEMPLATE_DUMMY_KEY: continue + if layer_number is None: + indexes = slice(i - 1, i + 3) + base[indexes] = " " + shift[indexes] = " " + continue + indexes = slice(i - 1, i + 1) base_key = " " @@ -470,6 +478,8 @@ def _get_geometry(self, layers: Optional[List[Layer]] = None) -> List[str]: rows = GEOMETRY[self.geometry].rows template = GEOMETRY[self.geometry].template.split("\n")[:-1] + # Clear template + template = self._fill_template(template, rows, None) for i in layers: template = self._fill_template(template, rows, i) return template From b4de9f08fae7db29c9a1460df60ab65ef8b09116 Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Sun, 7 Apr 2024 21:32:59 +0200 Subject: [PATCH 11/17] Hack for system symbols --- kalamine/generators/klc.py | 4 ++-- kalamine/layout.py | 11 +++++++++-- kalamine/utils.py | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/kalamine/generators/klc.py b/kalamine/generators/klc.py index 6831d65..a77af34 100644 --- a/kalamine/generators/klc.py +++ b/kalamine/generators/klc.py @@ -124,7 +124,7 @@ def klc_keymap(layout: "KeyboardLayout") -> List[str]: desc = layout.dead_keys[symbol][" "] symbol = hex_ord(desc) + "@" else: - if i == Layer.BASE: + if i is Layer.BASE: is_alpha = symbol.upper() != symbol if symbol not in supported_symbols: symbol = hex_ord(symbol) @@ -256,7 +256,7 @@ def c_keymap(layout: "KeyboardLayout") -> List[str]: dead = hex_ord(desc) has_dead_key = True else: - if i == Layer.BASE: + if i is Layer.BASE: is_alpha = symbol.upper() != symbol if symbol not in supported_symbols: symbol = hex_ord(symbol) diff --git a/kalamine/layout.py b/kalamine/layout.py index 5fdd82c..b4701c3 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -371,7 +371,7 @@ def _parse_template( ) -> None: """Extract a keyboard layer from a template.""" - col_offset = 0 if layer_number == Layer.BASE else 2 + col_offset = 0 if layer_number is Layer.BASE else 2 for j, row in enumerate(rows): keys = row.keys @@ -425,7 +425,7 @@ def _fill_template( if layer_number is None: col_offset = 0 - elif layer_number == Layer.BASE: + elif layer_number is Layer.BASE: col_offset = 0 shift_prevails = True else: # AltGr or 1dk @@ -458,6 +458,13 @@ def _fill_template( if key in self.layers[layer_number.next()]: shift_key = SpecialSymbol.prettify(self.layers[layer_number.next()][key]) + # Do not display system symbols if they are identical to the first levels + if layer_number is not Layer.BASE: + if SystemSymbol.is_system_symbol(base_key) and base_key == self.layers[Layer.BASE].get(key): + base_key = " " + if SystemSymbol.is_system_symbol(shift_key) and shift_key == self.layers[Layer.SHIFT].get(key): + shift_key = " " + if shift_prevails: shift[indexes] = shift_key.rjust(2) if pretty_upper_key(base_key, blank_if_obvious=False) != shift_key: diff --git a/kalamine/utils.py b/kalamine/utils.py index ab2d562..b0e550b 100644 --- a/kalamine/utils.py +++ b/kalamine/utils.py @@ -75,9 +75,9 @@ def next(self) -> "Layer": def necromance(self) -> "Layer": """Remove the effect of the dead key if any.""" - if self == Layer.ODK: + if self is Layer.ODK: return Layer.BASE - elif self == Layer.ODK_SHIFT: + elif self is Layer.ODK_SHIFT: return Layer.SHIFT return self From 5cb1ae20ec5db3803fcaf1a0065794a9b9360578 Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Mon, 8 Apr 2024 07:17:15 +0200 Subject: [PATCH 12/17] Add Numpad keys & more web key codes --- kalamine/data/scan_codes.yaml | 187 ++++++++++++++++++++++++++-------- kalamine/key.py | 43 ++++---- 2 files changed, 163 insertions(+), 67 deletions(-) diff --git a/kalamine/data/scan_codes.yaml b/kalamine/data/scan_codes.yaml index b19ab1c..c80ad14 100644 --- a/kalamine/data/scan_codes.yaml +++ b/kalamine/data/scan_codes.yaml @@ -264,155 +264,246 @@ SpaceBar: windows: "39" macos: "49" hand: null +Numpad: +- xkb: kp0 + web: Numpad0 + windows: null + macos: null + hand: null +- xkb: kp1 + web: Numpad1 + windows: null + macos: null + hand: null +- xkb: kp2 + web: Numpad2 + windows: null + macos: null + hand: null +- xkb: kp3 + web: Numpad3 + windows: null + macos: null + hand: null +- xkb: kp4 + web: Numpad4 + windows: null + macos: null + hand: null +- xkb: kp5 + web: Numpad5 + windows: null + macos: null + hand: null +- xkb: kp6 + web: Numpad6 + windows: null + macos: null + hand: null +- xkb: kp7 + web: Numpad7 + windows: null + macos: null + hand: null +- xkb: kp8 + web: Numpad8 + windows: null + macos: null + hand: null +- xkb: kp9 + web: Numpad9 + windows: null + macos: null + hand: null +- xkb: kpen + web: NumpadEnter + windows: null + macos: null + hand: null +- xkb: kpeq + web: NumpadEqual + windows: null + macos: null + hand: null +- xkb: kpdl + web: NumpadDecimal + windows: null + macos: null + hand: null +- xkb: kppt + web: NumpadComma + windows: null + macos: null + hand: null +- xkb: kpdv + web: NumpadDivide + windows: null + macos: null + hand: null +- xkb: kpmu + web: NumpadMultiply + windows: null + macos: null + hand: null +- xkb: kpsu + web: NumpadSubtract + windows: null + macos: null + hand: null +- xkb: kpad + web: NumpadAdd + windows: null + macos: null + hand: null +- xkb: nmlk + web: NumLock + windows: null + macos: null + hand: null System: - xkb: "tab" - web: null + web: Tab windows: null macos: null hand: Left - xkb: "rtrn" - web: null + web: Enter windows: null macos: null hand: null - xkb: "bksp" - web: null + web: Backspace windows: null macos: null hand: null - xkb: "dele" - web: null + web: Delete windows: null macos: null hand: null - xkb: "esc" - web: null + web: Escape windows: null macos: null hand: null - xkb: "menu" - web: null + web: ContextMenu windows: null macos: null hand: null - xkb: "home" - web: null + web: Home windows: null macos: null hand: null -- xkb: "up" - web: null +- xkb: "end" + web: End windows: null macos: null hand: null -- xkb: "end" - web: null +- xkb: "up" + web: ArrowUp windows: null macos: null hand: null -- xkb: "pgup" - web: null +- xkb: "down" + web: ArrowDown windows: null macos: null hand: null - xkb: "left" - web: null + web: ArrowLeft windows: null macos: null hand: null -- xkb: "down" - web: null +- xkb: "rght" + web: ArrowDown windows: null macos: null hand: null -- xkb: "rght" - web: null +- xkb: "pgup" + web: PageUp windows: null macos: null hand: null - xkb: "pgdn" - web: null + web: PageDown windows: null macos: null hand: null - xkb: "fk01" - web: null + web: F1 windows: null macos: null hand: null - xkb: "fk02" - web: null + web: F2 windows: null macos: null hand: null - xkb: "fk03" - web: null + web: F3 windows: null macos: null hand: null - xkb: "fk04" - web: null + web: F4 windows: null macos: null hand: null - xkb: "fk05" - web: null - windows: null - macos: null - hand: null -- xkb: "dele" - web: null + web: F5 windows: null macos: null hand: null - xkb: "fk06" - web: null + web: F6 windows: null macos: null hand: null - xkb: "fk07" - web: null + web: F7 windows: null macos: null hand: null - xkb: "fk08" - web: null + web: F8 windows: null macos: null hand: null - xkb: "fk09" - web: null + web: F9 windows: null macos: null hand: null - xkb: "fk10" - web: null + web: F10 windows: null macos: null hand: null - xkb: "fk11" - web: null + web: F11 windows: null macos: null hand: null - xkb: "fk12" - web: null + web: F12 windows: null macos: null hand: null Modifiers: - xkb: "lfsh" - web: null + web: ShiftLeft windows: null macos: null hand: Left - xkb: "rtsh" - web: null + web: ShiftRight windows: null macos: null hand: Right - xkb: "caps" - web: null + web: CapsLock windows: null macos: null hand: Left @@ -427,12 +518,22 @@ Modifiers: macos: null hand: Right - xkb: "lctl" - web: null + web: ControlLeft windows: null macos: null hand: Left - xkb: "rctl" - web: null + web: ControlRight + windows: null + macos: null + hand: Right +- xkb: lwin + web: MetaLeft + windows: null + macos: null + hand: Left +- xkb: rwin + web: MetaRight windows: null macos: null hand: Right @@ -460,12 +561,12 @@ Miscellaneous: macos: null hand: null - xkb: "i172" # Play/Pause - web: null + web: MediaPlayPause windows: null macos: null hand: null - xkb: "i180" # Home page - web: null + web: BrowserHome windows: null macos: null hand: null diff --git a/kalamine/key.py b/kalamine/key.py index a11214f..0cf3f34 100644 --- a/kalamine/key.py +++ b/kalamine/key.py @@ -27,6 +27,7 @@ class KeyCategory(Flag): Letters3 = auto() PinkyKeys = auto() SpaceBar = auto() + Numpad = auto() System = auto() Modifiers = auto() InputMethod = auto() @@ -36,36 +37,30 @@ class KeyCategory(Flag): @classmethod def parse(cls, raw: str) -> Self: for kc in cls: - if kc.name.casefold() == raw.casefold(): + if kc.name and kc.name.casefold() == raw.casefold(): return kc else: raise ValueError(f"Cannot parse key category: “{raw}”") @property def description(self) -> str: - match self: - case KeyCategory.Digits: - return "Digits" - case KeyCategory.Letters1: - return "Letters, first row" - case KeyCategory.Letters2: - return "Letters, second row" - case KeyCategory.Letters3: - return "Letters, third row" - case KeyCategory.PinkyKeys: - return "Pinky keys" - case KeyCategory.SpaceBar: - return "Space bar" - case KeyCategory.System: - return "System" - case KeyCategory.Modifiers: - return "Modifiers" - case KeyCategory.InputMethod: - return "Input method" - case KeyCategory.Miscellaneous: - return "Miscellaneous" - case _: - raise ValueError(f"No description ofr KeyCategory: {self}") + descriptions = { + KeyCategory.Digits: "Digits", + KeyCategory.Letters1: "Letters, first row", + KeyCategory.Letters2: "Letters, second row", + KeyCategory.Letters3: "Letters, third row", + KeyCategory.PinkyKeys: "Pinky keys", + KeyCategory.SpaceBar: "Space bar", + KeyCategory.Numpad: "Numeric pad", + KeyCategory.System: "System", + KeyCategory.Modifiers: "Modifiers", + KeyCategory.InputMethod: "Input method", + KeyCategory.Miscellaneous: "Miscellaneous", + } + if d := descriptions.get(self): + return d + else: + raise ValueError(f"No description ofr KeyCategory: {self}") @dataclass From ebf1dce54625e101fa33508de6202d7b018d3425 Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Mon, 8 Apr 2024 07:53:11 +0200 Subject: [PATCH 13/17] Fix XKB tests --- tests/test_serializer_xkb.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/test_serializer_xkb.py b/tests/test_serializer_xkb.py index 302e4a6..45319db 100644 --- a/tests/test_serializer_xkb.py +++ b/tests/test_serializer_xkb.py @@ -70,14 +70,11 @@ def test_ansi(): // Pinky keys key {[ minus , underscore , VoidSymbol , VoidSymbol ]}; // - _ key {[ equal , plus , VoidSymbol , VoidSymbol ]}; // = + - key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; // key {[ bracketleft , braceleft , VoidSymbol , VoidSymbol ]}; // [ { key {[ bracketright , braceright , VoidSymbol , VoidSymbol ]}; // ] } key {[ apostrophe , quotedbl , VoidSymbol , VoidSymbol ]}; // ' " - key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; // key {[ grave , asciitilde , VoidSymbol , VoidSymbol ]}; // ` ~ key {[ backslash , bar , VoidSymbol , VoidSymbol ]}; // \\ | - key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; // // Space bar key {[ space , space , apostrophe , apostrophe ]}; // ' ' @@ -149,11 +146,9 @@ def test_intl(): // Pinky keys key {[ minus , underscore , VoidSymbol , VoidSymbol ]}; // - _ key {[ equal , plus , VoidSymbol , VoidSymbol ]}; // = + - key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; // key {[ bracketleft , braceleft , VoidSymbol , VoidSymbol ]}; // [ { key {[ bracketright , braceright , VoidSymbol , VoidSymbol ]}; // ] } key {[ ISO_Level3_Latch, dead_diaeresis , apostrophe , VoidSymbol ]}; // ' ¨ ' - key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; // key {[ dead_grave , dead_tilde , VoidSymbol , VoidSymbol ]}; // ` ~ key {[ backslash , bar , VoidSymbol , VoidSymbol ]}; // \\ | key {[ backslash , bar , VoidSymbol , VoidSymbol ]}; // \\ | @@ -228,14 +223,11 @@ def test_prog(): // Pinky keys key {[ minus , underscore , VoidSymbol , VoidSymbol ]}; // - _ key {[ equal , plus , VoidSymbol , VoidSymbol ]}; // = + - key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; // key {[ bracketleft , braceleft , VoidSymbol , VoidSymbol ]}; // [ { key {[ bracketright , braceright , VoidSymbol , VoidSymbol ]}; // ] } key {[ apostrophe , quotedbl , dead_acute , dead_diaeresis ]}; // ' " ´ ¨ - key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; // key {[ grave , asciitilde , dead_grave , dead_tilde ]}; // ` ~ ` ~ key {[ backslash , bar , VoidSymbol , VoidSymbol ]}; // \\ | - key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; // // Space bar key {[ space , space , space , space ]}; // From a579412e9f21545848c93c9c27132565efb6b0c9 Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Mon, 8 Apr 2024 07:57:39 +0200 Subject: [PATCH 14/17] Add Windows scan codes --- kalamine/data/scan_codes.yaml | 218 +++++++++++++++++----------------- kalamine/generators/ahk.py | 32 +++-- kalamine/generators/klc.py | 35 ++++-- 3 files changed, 156 insertions(+), 129 deletions(-) diff --git a/kalamine/data/scan_codes.yaml b/kalamine/data/scan_codes.yaml index c80ad14..98a797e 100644 --- a/kalamine/data/scan_codes.yaml +++ b/kalamine/data/scan_codes.yaml @@ -1,551 +1,551 @@ digits: - xkb: "ae01" web: "Digit1" - windows: "02" + windows: "T02" macos: "18" hand: null - xkb: "ae02" web: "Digit2" - windows: "03" + windows: "T03" macos: "19" hand: null - xkb: "ae03" web: "Digit3" - windows: "04" + windows: "T04" macos: "20" hand: null - xkb: "ae04" web: "Digit4" - windows: "05" + windows: "T05" macos: "21" hand: null - xkb: "ae05" web: "Digit5" - windows: "06" + windows: "T06" macos: "23" hand: null - xkb: "ae06" web: "Digit6" - windows: "07" + windows: "T07" macos: "22" hand: null - xkb: "ae07" web: "Digit7" - windows: "08" + windows: "T08" macos: "26" hand: null - xkb: "ae08" web: "Digit8" - windows: "09" + windows: "T09" macos: "28" hand: null - xkb: "ae09" web: "Digit9" - windows: "0a" + windows: "T0A" macos: "25" hand: null - xkb: "ae10" web: "Digit0" - windows: "0b" + windows: "T0B" macos: "29" hand: null # Letters, first row Letters1: - xkb: "ad01" web: "KeyQ" - windows: "10" + windows: "T10" macos: "12" hand: null - xkb: "ad02" web: "KeyW" - windows: "11" + windows: "T11" macos: "13" hand: null - xkb: "ad03" web: "KeyE" - windows: "12" + windows: "T12" macos: "14" hand: null - xkb: "ad04" web: "KeyR" - windows: "13" + windows: "T13" macos: "15" hand: null - xkb: "ad05" web: "KeyT" - windows: "14" + windows: "T14" macos: "17" hand: null - xkb: "ad06" web: "KeyY" - windows: "15" + windows: "T15" macos: "16" hand: null - xkb: "ad07" web: "KeyU" - windows: "16" + windows: "T16" macos: "32" hand: null - xkb: "ad08" web: "KeyI" - windows: "17" + windows: "T17" macos: "34" hand: null - xkb: "ad09" web: "KeyO" - windows: "18" + windows: "T18" macos: "31" hand: null - xkb: "ad10" web: "KeyP" - windows: "19" + windows: "T19" macos: "35" hand: null # Letters, second row Letters2: - xkb: "ac01" web: "KeyA" - windows: "1e" + windows: "T1E" macos: "0" hand: null - xkb: "ac02" web: "KeyS" - windows: "1f" + windows: "T1F" macos: "1" hand: null - xkb: "ac03" web: "KeyD" - windows: "20" + windows: "T20" macos: "2" hand: null - xkb: "ac04" web: "KeyF" - windows: "21" + windows: "T21" macos: "3" hand: null - xkb: "ac05" web: "KeyG" - windows: "22" + windows: "T22" macos: "5" hand: null - xkb: "ac06" web: "KeyH" - windows: "23" + windows: "T23" macos: "4" hand: null - xkb: "ac07" web: "KeyJ" - windows: "24" + windows: "T24" macos: "38" hand: null - xkb: "ac08" web: "KeyK" - windows: "25" + windows: "T25" macos: "40" hand: null - xkb: "ac09" web: "KeyL" - windows: "26" + windows: "T26" macos: "37" hand: null - xkb: "ac10" web: "Semicolon" - windows: "27" + windows: "T27" macos: "41" hand: null # Letters, third row Letters3: - xkb: "ab01" web: "KeyZ" - windows: "2c" + windows: "T2C" macos: "6" hand: null - xkb: "ab02" web: "KeyX" - windows: "2d" + windows: "T2D" macos: "7" hand: null - xkb: "ab03" web: "KeyC" - windows: "2e" + windows: "T2E" macos: "8" hand: null - xkb: "ab04" web: "KeyV" - windows: "2f" + windows: "T2F" macos: "9" hand: null - xkb: "ab05" web: "KeyB" - windows: "30" + windows: "T30" macos: "11" hand: null - xkb: "ab06" web: "KeyN" - windows: "31" + windows: "T31" macos: "45" hand: null - xkb: "ab07" web: "KeyM" - windows: "32" + windows: "T32" macos: "46" hand: null - xkb: "ab08" web: "Comma" - windows: "33" + windows: "T33" macos: "43" hand: null - xkb: "ab09" web: "Period" - windows: "34" + windows: "T34" macos: "47" hand: null - xkb: "ab10" web: "Slash" - windows: "35" + windows: "T35" macos: "44" hand: null # Pinky keys PinkyKeys: - xkb: "ae11" web: "Minus" - windows: "0c" + windows: "T0C" macos: "27" hand: null - xkb: "ae12" web: "Equal" - windows: "0d" + windows: "T0D" macos: "24" hand: null - xkb: "ae13" web: "IntlYen" - windows: "0d" + windows: "T0D" macos: "42" hand: null - xkb: "ad11" web: "BracketLeft" - windows: "1a" + windows: "T1A" macos: "33" hand: null - xkb: "ad12" web: "BracketRight" - windows: "1b" + windows: "T1B" macos: "30" hand: null - xkb: "ac11" web: "Quote" - windows: "28" + windows: "T28" macos: "39" hand: null - xkb: "ab11" web: "IntlRo" - windows: "28" + windows: "T28" macos: "39" hand: null - xkb: "tlde" web: "Backquote" - windows: "29" + windows: "T29" macos: "50" hand: null - xkb: "bksl" web: "Backslash" - windows: "2b" + windows: "T2B" macos: "42" hand: null - xkb: "lsgt" web: "IntlBackslash" - windows: "56" + windows: "T56" macos: "10" hand: null # Space bar row SpaceBar: - xkb: "spce" web: "Space" - windows: "39" + windows: "T39" macos: "49" hand: null Numpad: - xkb: kp0 web: Numpad0 - windows: null + windows: T52 macos: null hand: null - xkb: kp1 web: Numpad1 - windows: null + windows: T4F macos: null hand: null - xkb: kp2 web: Numpad2 - windows: null + windows: T50 macos: null hand: null - xkb: kp3 web: Numpad3 - windows: null + windows: T51 macos: null hand: null - xkb: kp4 web: Numpad4 - windows: null + windows: T4B macos: null hand: null - xkb: kp5 web: Numpad5 - windows: null + windows: T4C macos: null hand: null - xkb: kp6 web: Numpad6 - windows: null + windows: T4D macos: null hand: null - xkb: kp7 web: Numpad7 - windows: null + windows: T47 macos: null hand: null - xkb: kp8 web: Numpad8 - windows: null + windows: T48 macos: null hand: null - xkb: kp9 web: Numpad9 - windows: null + windows: T49 macos: null hand: null - xkb: kpen web: NumpadEnter - windows: null + windows: X1C macos: null hand: null - xkb: kpeq web: NumpadEqual - windows: null + windows: T59 macos: null hand: null - xkb: kpdl web: NumpadDecimal - windows: null + windows: T53 macos: null hand: null - xkb: kppt web: NumpadComma - windows: null + windows: T7E macos: null hand: null - xkb: kpdv web: NumpadDivide - windows: null + windows: X35 macos: null hand: null - xkb: kpmu web: NumpadMultiply - windows: null + windows: T37 macos: null hand: null - xkb: kpsu web: NumpadSubtract - windows: null + windows: T4A macos: null hand: null - xkb: kpad web: NumpadAdd - windows: null + windows: T4E macos: null hand: null - xkb: nmlk web: NumLock - windows: null + windows: T45 macos: null hand: null System: - xkb: "tab" web: Tab - windows: null + windows: T0F macos: null hand: Left - xkb: "rtrn" web: Enter - windows: null + windows: T1C macos: null hand: null - xkb: "bksp" web: Backspace - windows: null + windows: T0E macos: null hand: null - xkb: "dele" web: Delete - windows: null + windows: X53 macos: null hand: null - xkb: "esc" web: Escape - windows: null + windows: T01 macos: null hand: null - xkb: "menu" web: ContextMenu - windows: null + windows: X5D macos: null hand: null - xkb: "home" web: Home - windows: null + windows: X47 macos: null hand: null - xkb: "end" web: End - windows: null + windows: X4F macos: null hand: null - xkb: "up" web: ArrowUp - windows: null + windows: X48 macos: null hand: null - xkb: "down" web: ArrowDown - windows: null + windows: X50 macos: null hand: null - xkb: "left" web: ArrowLeft - windows: null + windows: X4B macos: null hand: null - xkb: "rght" web: ArrowDown - windows: null + windows: X50 macos: null hand: null - xkb: "pgup" web: PageUp - windows: null + windows: X49 macos: null hand: null - xkb: "pgdn" web: PageDown - windows: null + windows: X51 macos: null hand: null - xkb: "fk01" web: F1 - windows: null + windows: T3B macos: null hand: null - xkb: "fk02" web: F2 - windows: null + windows: T3C macos: null hand: null - xkb: "fk03" web: F3 - windows: null + windows: T3D macos: null hand: null - xkb: "fk04" web: F4 - windows: null + windows: T3E macos: null hand: null - xkb: "fk05" web: F5 - windows: null + windows: T3F macos: null hand: null - xkb: "fk06" web: F6 - windows: null + windows: T40 macos: null hand: null - xkb: "fk07" web: F7 - windows: null + windows: T41 macos: null hand: null - xkb: "fk08" web: F8 - windows: null + windows: T42 macos: null hand: null - xkb: "fk09" web: F9 - windows: null + windows: T43 macos: null hand: null - xkb: "fk10" web: F10 - windows: null + windows: T44 macos: null hand: null - xkb: "fk11" web: F11 - windows: null + windows: T57 macos: null hand: null - xkb: "fk12" web: F12 - windows: null + windows: T58 macos: null hand: null Modifiers: - xkb: "lfsh" web: ShiftLeft - windows: null + windows: T2A macos: null hand: Left - xkb: "rtsh" web: ShiftRight - windows: null + windows: T36 macos: null hand: Right - xkb: "caps" web: CapsLock - windows: null + windows: T3A macos: null hand: Left - xkb: "lalt" web: "AltLeft" - windows: null + windows: T38 macos: null hand: Left - xkb: "ralt" web: "AltRight" - windows: null + windows: X38 macos: null hand: Right - xkb: "lctl" web: ControlLeft - windows: null + windows: T1D macos: null hand: Left - xkb: "rctl" web: ControlRight - windows: null + windows: X1D macos: null hand: Right - xkb: lwin web: MetaLeft - windows: null + windows: X5B macos: null hand: Left - xkb: rwin web: MetaRight - windows: null + windows: X5C macos: null hand: Right InputMethod: - xkb: "muhe" web: "NonConvert" - windows: null + windows: T7B macos: null hand: Left - xkb: "henk" web: "Convert" - windows: null + windows: T79 macos: null hand: Right # Miscellaneous @@ -562,11 +562,11 @@ Miscellaneous: hand: null - xkb: "i172" # Play/Pause web: MediaPlayPause - windows: null + windows: X22 macos: null hand: null - xkb: "i180" # Home page web: BrowserHome - windows: null + windows: X32 macos: null hand: null diff --git a/kalamine/generators/ahk.py b/kalamine/generators/ahk.py index f564f7f..74b323f 100644 --- a/kalamine/generators/ahk.py +++ b/kalamine/generators/ahk.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from ..layout import KeyboardLayout -from ..key import KEYS, KeyCategory +from ..key import KEYS, Key, KeyCategory from ..template import load_tpl, substitute_lines from ..utils import Layer, load_data @@ -22,6 +22,7 @@ def ahk_keymap(layout: "KeyboardLayout", altgr: bool = False) -> List[str]: prefixes = [" ", "+", "", "", " <^>!", "<^>!+"] specials = " \u00a0\u202f‘’'\"^`~" esc_all = True # set to False to ease the debug (more readable AHK script) + layers = (Layer.ALTGR, Layer.ALTGR_SHIFT) if altgr else (Layer.BASE, Layer.SHIFT) def ahk_escape(key: str) -> str: if len(key) == 1: @@ -44,18 +45,21 @@ def ahk_actions(symbol: str) -> Dict[str, str]: # TODO: delete test? # if key.id in ["ae13", "ab11"]: # ABNT / JIS keys # continue # these two keys are not supported yet - if key.windows is None: + # TODO: add support for all scan codes + if key.windows is None or not key.windows.startswith("T"): + continue + + # Skip key if not defined and is not alphanumeric + if not any(key.id in layout.layers[i] for i in layers) and not key.alphanum: continue if key.category is not prev_category: - output.append(f"; {key.category.description}") + output.append(f"; {key.category.description}") output.append("") prev_category = key.category - sc = f"SC{key.windows}" - for i in ( - [Layer.ALTGR, Layer.ALTGR_SHIFT] if altgr else [Layer.BASE, Layer.SHIFT] - ): + sc = f"SC{key.windows[1:].lower()}" + for i in layers: layer = layout.layers[i] if key.id not in layer: continue @@ -86,22 +90,28 @@ def ahk_shortcuts(layout: "KeyboardLayout") -> List[str]: prefixes = [" ^", "^+"] enabled = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" qwerty_vk = load_data("qwerty_vk") + layers = (Layer.BASE, Layer.SHIFT) output = [] prev_category: Optional[KeyCategory] = None for key in KEYS.values(): # if key_name in ["ae13", "ab11"]: # ABNT / JIS keys # continue # these two keys are not supported yet - if key.windows is None: + # TODO: add support for all scan codes + if key.windows is None or not key.windows.startswith("T"): + continue + + # Skip key if not defined and is not alphanumeric + if not any(key.id in layout.layers[i] for i in layers) and not key.alphanum: continue if key.category is not prev_category: - output.append(f"; {key.category.description}") + output.append(f"; {key.category.description}") output.append("") prev_category = key.category - scan_code = key.windows - for i in [Layer.BASE, Layer.SHIFT]: + scan_code = key.windows[1:].lower() + for i in layers: layer = layout.layers[i] if key.id not in layer: continue diff --git a/kalamine/generators/klc.py b/kalamine/generators/klc.py index a77af34..63f52ab 100644 --- a/kalamine/generators/klc.py +++ b/kalamine/generators/klc.py @@ -102,19 +102,24 @@ def klc_keymap(layout: "KeyboardLayout") -> List[str]: oem_idx = 0 # Python trick to do equivalent of C static variable output = [] qwerty_vk = load_data("qwerty_vk") + layers = (Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT) for key in KEYS.values(): if key.id in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet - if key.windows is None: + if key.windows is None or not key.windows.startswith("T"): # TODO: warning continue + # Skip key if not defined and is not alphanumeric + if not any(key.id in layout.layers[i] for i in layers) and not key.alphanum: + continue + symbols = [] description = "//" is_alpha = False - for i in [Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT]: + for i in layers: layer = layout.layers[i] if key.id in layer: @@ -134,7 +139,7 @@ def klc_keymap(layout: "KeyboardLayout") -> List[str]: symbols.append("-1") description += " " + desc - scan_code = key.windows + scan_code = key.windows[1:].lower() virtual_key = qwerty_vk[scan_code] if not layout.qwerty_shortcuts: @@ -186,7 +191,9 @@ def klc_deadkeys(layout: "KeyboardLayout") -> List[str]: continue dk = layout.dead_keys[k] - output.append(f"// DEADKEY: {layout.custom_dead_keys[k].name.upper()} //" + "{{{") + output.append( + f"// DEADKEY: {layout.custom_dead_keys[k].name.upper()} //" + "{{{" + ) output.append(f"DEADKEY\t{hex_ord(dk[' '])}") for base, alt in dk.items(): @@ -218,7 +225,9 @@ def klc_dk_index(layout: "KeyboardLayout") -> List[str]: if k not in layout.dead_keys: continue dk = layout.dead_keys[k] - output.append(f"{hex_ord(dk[' '])}\t\"{layout.custom_dead_keys[k].name.upper()}\"") + output.append( + f"{hex_ord(dk[' '])}\t\"{layout.custom_dead_keys[k].name.upper()}\"" + ) return output @@ -227,6 +236,7 @@ def c_keymap(layout: "KeyboardLayout") -> List[str]: supported_symbols = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" qwerty_vk = load_data("qwerty_vk") + layers = (Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT) global oem_idx oem_idx = 0 # Python trick to do equivalent of C static variable @@ -234,16 +244,21 @@ def c_keymap(layout: "KeyboardLayout") -> List[str]: for key in KEYS.values(): if key.id in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet - if key.windows is None: + # TODO: add support for all scan codes + if key.windows is None or not key.windows.startswith("T"): # TODO: warning continue + # Skip key if not defined and is not alphanumeric + if not any(key.id in layout.layers[i] for i in layers) and not key.alphanum: + continue + symbols = [] dead_symbols = [] is_alpha = False has_dead_key = False - for i in [Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT]: + for i in layers: layer = layout.layers[i] if key.id in layer: @@ -267,7 +282,7 @@ def c_keymap(layout: "KeyboardLayout") -> List[str]: symbols.append("WCH_NONE") dead_symbols.append("WCH_NONE") - scan_code = key.windows + scan_code = key.windows[1:].lower() virtual_key = qwerty_vk[scan_code] if not layout.qwerty_shortcuts: @@ -363,7 +378,9 @@ def c_dk_index(layout: "KeyboardLayout") -> List[str]: if k not in layout.dead_keys: continue term = layout.dead_keys[k][" "] - output.append(f'L"\\\\x{hex_ord(term)}"\tL"{layout.custom_dead_keys[k].name.upper()}",') + output.append( + f'L"\\\\x{hex_ord(term)}"\tL"{layout.custom_dead_keys[k].name.upper()}",' + ) return output From e6edc7178f8f6a62c665ad61c22cb173045adc58 Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Mon, 8 Apr 2024 08:53:31 +0200 Subject: [PATCH 15/17] test(XKB): Add extra mapping test --- tests/test_serializer_xkb.py | 54 ++++++++++++++++++++++++++++-------- tests/util.py | 11 ++++++-- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/tests/test_serializer_xkb.py b/tests/test_serializer_xkb.py index 45319db..febf4f5 100644 --- a/tests/test_serializer_xkb.py +++ b/tests/test_serializer_xkb.py @@ -1,4 +1,5 @@ from textwrap import dedent +from typing import Dict from kalamine import KeyboardLayout from kalamine.generators.xkb import xkb_table @@ -6,8 +7,10 @@ from .util import get_layout_dict -def load_layout(filename: str) -> KeyboardLayout: - return KeyboardLayout(get_layout_dict(filename)) +def load_layout( + filename: str, extraMapping: Dict[str, Dict[str, str]] +) -> KeyboardLayout: + return KeyboardLayout(get_layout_dict(filename, extraMapping)) def split(multiline_str: str): @@ -15,7 +18,7 @@ def split(multiline_str: str): def test_ansi(): - layout = load_layout("ansi") + layout = load_layout("ansi", {}) expected = split( """ @@ -91,8 +94,6 @@ def test_ansi(): def test_intl(): - layout = load_layout("intl") - expected = split( """ // Digits @@ -158,17 +159,46 @@ def test_intl(): """ ) - xkbcomp = xkb_table(layout, xkbcomp=True) - assert len(xkbcomp) == len(expected) - assert xkbcomp == expected + extraMapping = { + # NOTE: redefine level + "ae01": {"shift": "?"}, + # NOTE: test case variants and ODK alias + "menu": {"base": "a", "sHiFt": "A", "1dk": "æ", "ODk_shiFt": "Æ"}, + # NOTE: clone level + "esc": {"base": "(ae11)"}, + # NOTE: clone key + "i172": "(lsgt)", + } + + extraSymbols = [ + "", + "// System", + "key {[ minus , VoidSymbol , VoidSymbol , VoidSymbol ]}; // -", + "key {[ a , A , ae , AE ]}; // a A æ Æ", + "", + "// Miscellaneous", + "key {[ backslash , bar , VoidSymbol , VoidSymbol ]}; // \\ |", + ] + + extraExpected = expected + extraSymbols + extraExpected[1] = ( + "key {[ 1 , question , VoidSymbol , VoidSymbol ]}; // 1 ?" + ) - xkbpatch = xkb_table(layout, xkbcomp=False) - assert len(xkbpatch) == len(expected) - assert xkbpatch == expected + for mapping, expectedʹ in (({}, expected), (extraMapping, extraExpected)): + layout = load_layout("intl", mapping) + + xkbcomp = xkb_table(layout, xkbcomp=True) + assert len(xkbcomp) == len(expectedʹ) + assert xkbcomp == expectedʹ + + xkbpatch = xkb_table(layout, xkbcomp=False) + assert len(xkbpatch) == len(expectedʹ) + assert xkbpatch == expectedʹ def test_prog(): - layout = load_layout("prog") + layout = load_layout("prog", {}) expected = split( """ diff --git a/tests/util.py b/tests/util.py index c4b35f2..45a8d0b 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,14 +1,19 @@ """Some util functions for tests.""" from pathlib import Path -from typing import Dict +from typing import Dict, Optional import tomli -def get_layout_dict(filename: str) -> Dict: +def get_layout_dict( + filename: str, extraMapping: Optional[Dict[str, Dict[str, str]]] = None +) -> Dict: """Return the layout directory path.""" file_path = Path(__file__).parent.parent / f"layouts/{filename}.toml" with file_path.open(mode="rb") as file: - return tomli.load(file) + layout = tomli.load(file) + if extraMapping: + layout.update({"mapping": extraMapping}) + return layout From 2505a4365c72531cde34dc9a7464e8f6760f8068 Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Mon, 8 Apr 2024 10:39:18 +0200 Subject: [PATCH 16/17] Improve Windows VK --- kalamine/data/qwerty_vk.yaml | 169 ++++++++++++++++++++++++----------- kalamine/generators/ahk.py | 2 +- kalamine/generators/klc.py | 70 +++++++++++---- tests/test_serializer_klc.py | 29 +++++- 4 files changed, 201 insertions(+), 69 deletions(-) diff --git a/kalamine/data/qwerty_vk.yaml b/kalamine/data/qwerty_vk.yaml index 1317f3d..9fd2e2d 100644 --- a/kalamine/data/qwerty_vk.yaml +++ b/kalamine/data/qwerty_vk.yaml @@ -1,61 +1,130 @@ # Scancodes <-> Virtual Keys as in qwerty # this is to keep shortcuts at their qwerty location -'39': 'SPACE' +'T39': 'SPACE' # digits -'02': '1' -'03': '2' -'04': '3' -'05': '4' -'06': '5' -'07': '6' -'08': '7' -'09': '8' -'0a': '9' -'0b': '0' +'T02': '1' +'T03': '2' +'T04': '3' +'T05': '4' +'T06': '5' +'T07': '6' +'T08': '7' +'T09': '8' +'T0A': '9' +'T0B': '0' # letters, first row -'10': 'Q' -'11': 'W' -'12': 'E' -'13': 'R' -'14': 'T' -'15': 'Y' -'16': 'U' -'17': 'I' -'18': 'O' -'19': 'P' - +'T10': 'Q' +'T11': 'W' +'T12': 'E' +'T13': 'R' +'T14': 'T' +'T15': 'Y' +'T16': 'U' +'T17': 'I' +'T18': 'O' +'T19': 'P' + # letters, second row -'1e': 'A' -'1f': 'S' -'20': 'D' -'21': 'F' -'22': 'G' -'23': 'H' -'24': 'J' -'25': 'K' -'26': 'L' -'27': 'OEM_1' +'T1E': 'A' +'T1F': 'S' +'T20': 'D' +'T21': 'F' +'T22': 'G' +'T23': 'H' +'T24': 'J' +'T25': 'K' +'T26': 'L' +'T27': 'OEM_1' # letters, third row -'2c': 'Z' -'2d': 'X' -'2e': 'C' -'2f': 'V' -'30': 'B' -'31': 'N' -'32': 'M' -'33': 'OEM_COMMA' -'34': 'OEM_PERIOD' -'35': 'OEM_2' +'T2C': 'Z' +'T2D': 'X' +'T2E': 'C' +'T2F': 'V' +'T30': 'B' +'T31': 'N' +'T32': 'M' +'T33': 'OEM_COMMA' +'T34': 'OEM_PERIOD' +'T35': 'OEM_2' # pinky keys -'29': 'OEM_3' -'0c': 'OEM_MINUS' -'0d': 'OEM_PLUS' -'1a': 'OEM_4' -'1b': 'OEM_6' -'28': 'OEM_7' -'2b': 'OEM_5' -'56': 'OEM_102' \ No newline at end of file +'T29': 'OEM_3' +'T0C': 'OEM_MINUS' +'T0D': 'OEM_PLUS' +'T1A': 'OEM_4' +'T1B': 'OEM_6' +'T28': 'OEM_7' +'T2B': 'OEM_5' +'T56': 'OEM_102' + +# Numpad +'T52': 'NUMPAD0' +'T4F': 'NUMPAD1' +'T50': 'NUMPAD2' +'T51': 'NUMPAD3' +'T4B': 'NUMPAD4' +'T4C': 'NUMPAD5' +'T4D': 'NUMPAD6' +'T47': 'NUMPAD7' +'T48': 'NUMPAD8' +'T49': 'NUMPAD9' +'T37': 'MULTIPLY' +'T4E': 'ADD' +'T4A': 'SUBTRACT' +'X35': 'DIVIDE' +'T53': 'DECIMAL' +# 'X1C': '' # NumpadEnter (maps to Return) +# 'T59': 'VK_CLEAR' # NumpadEqual (VK not mappable) +'T7E': 'VK_ABNT_C2' # NumadComma +# 'T45': 'NUMLOCK' # (VK not mappable) + +# System +'T0F': 'TAB' +'T1C': 'RETURN' +'T0E': 'BACK' # Backspace +# 'X52': 'INSERT' # (VK not mappable) +# 'X53': 'DELETE' # (VK not mappable) +# 'T3B': 'F1' # (VK not mappable) +# 'T3C': 'F2' # (VK not mappable) +# 'T3D': 'F3' # (VK not mappable) +# 'T3E': 'F4' # (VK not mappable) +# 'T3F': 'F5' # (VK not mappable) +# 'T40': 'F6' # (VK not mappable) +# 'T41': 'F7' # (VK not mappable) +# 'T42': 'F8' # (VK not mappable) +# 'T43': 'F9' # (VK not mappable) +# 'T44': 'F10' # (VK not mappable) +# 'T57': 'F11' # (VK not mappable) +# 'T58': 'F12' # (VK not mappable) +# 'X47': 'HOME' # (VK not mappable) +# 'X4F': 'END' # (VK not mappable) +# 'X49': 'PRIOR' # PageUp (VK not mappable) +# 'X51': 'NEXT' # PageDown (VK not mappable) +# 'T01': 'ESCAPE' # (VK not mappable) +# 'X48': 'UP' # (VK not mappable) +# 'X50': 'DOWN' # (VK not mappable) +# 'X4B': 'LEFT' # (VK not mappable) +# 'X4D': 'RIGHT' # (VK not mappable) +# 'X5D': 'APPS' # ContextMenu (VK not mappable) + +# Modifiers +# 'T2A': 'LSHIFT' # ShiftLeft (VK not mappable) +# 'T36': 'RSHIFT' # ShiftRight (VK not mappable) +# 'T3A': 'CAPITAL' # CapsLock (VK not mappable) +# 'T1D': 'LCONTROL' # ControlLeft (VK not mappable) +# 'X1D': 'RCONTROL' # ControlRight (VK not mappable) +# 'T38': 'LMENU' # AltLeft (VK not mappable) +# 'X38': 'RMENU' # AltRight (VK not mappable) +# 'X5B': 'LWIN' # MetaLeft (VK not mappable) +# 'X5C': 'RWIN' # MetaRight (VK not mappable) + +# Input method +# 'T7B': 'NONCONVERT' # Muhenkan (VK not mappable) +# 'T79': 'CONVERT' # Henkan (VK not mappable) + +# Miscellaneous +# 'X22': 'MEDIA_PLAY_PAUSE' # (VK not mappable) +# 'X32': 'BROWSER_HOME' # (VK not mappable) \ No newline at end of file diff --git a/kalamine/generators/ahk.py b/kalamine/generators/ahk.py index 74b323f..315a875 100644 --- a/kalamine/generators/ahk.py +++ b/kalamine/generators/ahk.py @@ -118,7 +118,7 @@ def ahk_shortcuts(layout: "KeyboardLayout") -> List[str]: symbol = layer[key.id] if layout.qwerty_shortcuts: - symbol = qwerty_vk[scan_code] + symbol = qwerty_vk[key.windows] if symbol in enabled: output.append(f"{prefixes[i]}SC{scan_code}::Send {prefixes[i]}{symbol}") diff --git a/kalamine/generators/klc.py b/kalamine/generators/klc.py index 63f52ab..3ae7987 100644 --- a/kalamine/generators/klc.py +++ b/kalamine/generators/klc.py @@ -48,38 +48,62 @@ def _get_langid(locale: str) -> str: oem_idx = 0 -def klc_virtual_key(layout: "KeyboardLayout", symbols: list, scan_code: str) -> str: - if scan_code == "56": +def check_virtual_key_symbols( + virtual_keys: Dict[str, List[str]], vk: str, symbols: List[str] +): + return (symbolsʹ := virtual_keys.get(vk)) is None or symbolsʹ == symbols + + +def klc_virtual_key( + layout: "KeyboardLayout", + symbols: list, + scan_code: str, + virtual_keys: Dict[str, List[str]], +) -> str: + if scan_code == "T56": # manage the ISO key (between shift and Z on ISO keyboards). # We're assuming that its scancode is always 56 # https://www.win.tue.nl/~aeb/linux/kbd/scancodes.html return "OEM_102" + # Check that the target VK is not already assigned to different symbols + def check(vk: str): + return check_virtual_key_symbols(virtual_keys, vk, symbols) + + # TODO: add support for Numpad keys base = _get_chr(symbols[0]) shifted = _get_chr(symbols[1]) # Can’t use `isdigit()` because `²` is a digit but we don't want that as a VK allowed_digit = "0123456789" # We assume that digit row always have digit as VK - if base in allowed_digit: + if base in allowed_digit and check(base): return base - elif shifted in allowed_digit: + elif shifted in allowed_digit and check(shifted): return shifted if shifted.isascii() and shifted.isalpha(): return shifted # VK_OEM_* case - if base == "," or shifted == ",": + if (base == "," or shifted == ",") and check("OEM_COMMA"): return "OEM_COMMA" - elif base == "." or shifted == ".": + elif (base == "." or shifted == ".") and check("OEM_PERIOD"): return "OEM_PERIOD" - elif base == "+" or shifted == "+": + elif (base == "+" or shifted == "+") and check("OEM_PLUS"): return "OEM_PLUS" - elif base == "-" or shifted == "-": + elif (base == "-" or shifted == "-") and check("OEM_MINUS"): return "OEM_MINUS" - elif base == " ": + elif base == " " and check("SPACE"): return "SPACE" + elif base == "\t" and check("TAB"): + return "TAB" + elif base == "\r" and check("RETURN"): + return "RETURN" + elif base == "\b" and check("BACK"): + return "BACK" + elif base == "\x1b" and check("ESCAPE"): + return "ESCAPE" else: MAX_OEM = 8 # We affect abitrary OEM VK and it will not match the one @@ -103,6 +127,7 @@ def klc_keymap(layout: "KeyboardLayout") -> List[str]: output = [] qwerty_vk = load_data("qwerty_vk") layers = (Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT) + virtual_keys: Dict[str, List[str]] = {} for key in KEYS.values(): if key.id in ["ae13", "ab11"]: # ABNT / JIS keys @@ -141,9 +166,15 @@ def klc_keymap(layout: "KeyboardLayout") -> List[str]: scan_code = key.windows[1:].lower() - virtual_key = qwerty_vk[scan_code] - if not layout.qwerty_shortcuts: - virtual_key = klc_virtual_key(layout, symbols, scan_code) + if ( + layout.qwerty_shortcuts + and key.windows in qwerty_vk + and check_virtual_key_symbols(virtual_keys, qwerty_vk[key.windows], symbols) + ): + virtual_key = qwerty_vk[key.windows] + else: + virtual_key = klc_virtual_key(layout, symbols, key.windows, virtual_keys) + virtual_keys[virtual_key] = symbols if layout.has_altgr: output.append( @@ -237,6 +268,7 @@ def c_keymap(layout: "KeyboardLayout") -> List[str]: supported_symbols = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" qwerty_vk = load_data("qwerty_vk") layers = (Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT) + virtual_keys: Dict[str, List[str]] = {} global oem_idx oem_idx = 0 # Python trick to do equivalent of C static variable @@ -282,11 +314,17 @@ def c_keymap(layout: "KeyboardLayout") -> List[str]: symbols.append("WCH_NONE") dead_symbols.append("WCH_NONE") - scan_code = key.windows[1:].lower() + # scan_code = key.windows[1:].lower() - virtual_key = qwerty_vk[scan_code] - if not layout.qwerty_shortcuts: - virtual_key = klc_virtual_key(layout, symbols, scan_code) + if ( + layout.qwerty_shortcuts + and key.windows in qwerty_vk + and check_virtual_key_symbols(virtual_keys, qwerty_vk[key.windows], symbols) + ): + virtual_key = qwerty_vk[key.windows] + else: + virtual_key = klc_virtual_key(layout, symbols, key.windows, virtual_keys) + virtual_keys[virtual_key] = symbols if len(virtual_key) == 1: virtual_key_id = f"'{virtual_key}'" diff --git a/tests/test_serializer_klc.py b/tests/test_serializer_klc.py index ce916ae..8153686 100644 --- a/tests/test_serializer_klc.py +++ b/tests/test_serializer_klc.py @@ -80,8 +80,7 @@ def test_ansi_deadkeys(): def test_intl_keymap(): keymap = klc_keymap(LAYOUTS["intl"]) - assert len(keymap) == 49 - assert keymap == split( + keymap_ref = split( """ 02 1 0 1 0021 -1 -1 // 1 ! 03 2 0 2 0040 -1 -1 // 2 @ @@ -134,6 +133,32 @@ def test_intl_keymap(): 39 SPACE 0 0020 0020 -1 -1 // """ ) + assert len(keymap) == len(keymap_ref) + assert keymap == keymap_ref + + extraMapping = { + # NOTE: redefine level + "ae01": {"shift": "?"}, + # TODO + # # NOTE: test case variants and ODK alias + # "kppt": {"base": ",", "sHiFt": ";", "1dk": ".", "ODk_shiFt": ":"}, + # NOTE: clone level + "esc": {"base": "\x1b", "shift": "(ae11)"}, + # NOTE: clone key + "henk": "(lsgt)", + } + + extraSymbols = [ + "01 ESCAPE 0 001b 005f -1 -1 // \x1b _", + "79 OEM_8 0 005c 007c -1 -1 // \\ |", + ] + keymap_extra_ref = keymap_ref + extraSymbols + keymap_extra_ref[0] = "02 1 0 1 003f -1 -1 // 1 ?" + + layout = KeyboardLayout(get_layout_dict("intl", extraMapping)) + keymap = klc_keymap(layout) + assert len(keymap) == len(keymap_extra_ref) + assert keymap == keymap_extra_ref def test_intl_deadkeys(): From e71c35a7101bffaf6852aa6f82510e36e529013b Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Mon, 8 Apr 2024 10:39:34 +0200 Subject: [PATCH 17/17] Windows VK fixes for System symbols --- kalamine/generators/klc.py | 65 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/kalamine/generators/klc.py b/kalamine/generators/klc.py index 3ae7987..05610a1 100644 --- a/kalamine/generators/klc.py +++ b/kalamine/generators/klc.py @@ -11,7 +11,7 @@ """ import re -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, Dict, List, Optional if TYPE_CHECKING: @@ -19,7 +19,7 @@ from ..key import KEYS from ..template import load_tpl, substitute_lines, substitute_token -from ..utils import Layer, hex_ord, load_data +from ..utils import Layer, SystemSymbol, hex_ord, load_data # return the corresponding char for a symbol @@ -117,6 +117,33 @@ def check(vk: str): raise Exception("Too many OEM keys") +SYSTEM_SYMBOLS: Dict[str, Optional[str]] = { + SystemSymbol.Alt.value: None, + SystemSymbol.AltGr.value: None, + SystemSymbol.BackSpace.value: "\b", + SystemSymbol.CapsLock.value: None, + SystemSymbol.Compose.value: None, + SystemSymbol.Control.value: None, + SystemSymbol.Delete.value: None, + SystemSymbol.Escape.value: "\x1b", + SystemSymbol.Return.value: "\r", + SystemSymbol.Shift.value: None, + SystemSymbol.Super.value: None, + SystemSymbol.Tab.value: "\t", + SystemSymbol.RightTab.value: None, + SystemSymbol.LeftTab.value: None, + SystemSymbol.ArrowUp.value: None, + SystemSymbol.ArrowDown.value: None, + SystemSymbol.ArrowLeft.value: None, + SystemSymbol.ArrowRight.value: None, + SystemSymbol.PageUp.value: None, + SystemSymbol.PageDown.value: None, + SystemSymbol.Home.value: None, + SystemSymbol.End.value: None, + SystemSymbol.Menu.value: None, +} + + def klc_keymap(layout: "KeyboardLayout") -> List[str]: """Windows layout, main part.""" @@ -143,6 +170,7 @@ def klc_keymap(layout: "KeyboardLayout") -> List[str]: symbols = [] description = "//" is_alpha = False + base_system_action_error = False for i in layers: layer = layout.layers[i] @@ -153,6 +181,18 @@ def klc_keymap(layout: "KeyboardLayout") -> List[str]: if symbol in layout.dead_keys: desc = layout.dead_keys[symbol][" "] symbol = hex_ord(desc) + "@" + elif symbol in SYSTEM_SYMBOLS: + symbolʹ = SYSTEM_SYMBOLS[symbol] + if symbolʹ is None: + print( + f"Warning: unsupported system action for key {key.id}: {symbol}" + ) + if i is Layer.BASE: + base_system_action_error = True + symbol = "-1" + else: + symbol = hex_ord(symbolʹ) + desc = symbol else: if i is Layer.BASE: is_alpha = symbol.upper() != symbol @@ -164,6 +204,10 @@ def klc_keymap(layout: "KeyboardLayout") -> List[str]: symbols.append("-1") description += " " + desc + if base_system_action_error: + print(f"Warning: cannot map non-system action on system key: {key.id}") + continue + scan_code = key.windows[1:].lower() if ( @@ -289,6 +333,7 @@ def c_keymap(layout: "KeyboardLayout") -> List[str]: dead_symbols = [] is_alpha = False has_dead_key = False + base_system_action_error = False for i in layers: layer = layout.layers[i] @@ -302,6 +347,18 @@ def c_keymap(layout: "KeyboardLayout") -> List[str]: symbol = "WCH_DEAD" dead = hex_ord(desc) has_dead_key = True + elif symbol in SYSTEM_SYMBOLS: + symbolʹ = SYSTEM_SYMBOLS[symbol] + if symbolʹ is None: + print( + f"Warning: unsupported system action for key {key.id}: {symbol}" + ) + if i is Layer.BASE: + base_system_action_error = True + symbol = "-1" + else: + symbol = hex_ord(symbolʹ) + desc = symbol else: if i is Layer.BASE: is_alpha = symbol.upper() != symbol @@ -314,6 +371,10 @@ def c_keymap(layout: "KeyboardLayout") -> List[str]: symbols.append("WCH_NONE") dead_symbols.append("WCH_NONE") + if base_system_action_error: + print(f"Warning: cannot map non-system action on system key: {key.id}") + continue + # scan_code = key.windows[1:].lower() if (