diff --git a/kalamine/data/qwerty_vk.yaml b/kalamine/data/qwerty_vk.yaml index 1317f3d..b114d9e 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/data/scan_codes.yaml b/kalamine/data/scan_codes.yaml index e3aefd2..a24ded9 100644 --- a/kalamine/data/scan_codes.yaml +++ b/kalamine/data/scan_codes.yaml @@ -1,188 +1,125 @@ -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' +Digits: +- {web: Digit1, xkb: ae01, windows: T02, macos: 18, hand: null} +- {web: Digit2, xkb: ae02, windows: T03, macos: 19, hand: null} +- {web: Digit3, xkb: ae03, windows: T04, macos: 20, hand: null} +- {web: Digit4, xkb: ae04, windows: T05, macos: 21, hand: null} +- {web: Digit5, xkb: ae05, windows: T06, macos: 23, hand: null} +- {web: Digit6, xkb: ae06, windows: T07, macos: 22, hand: null} +- {web: Digit7, xkb: ae07, windows: T08, macos: 26, hand: null} +- {web: Digit8, xkb: ae08, windows: T09, macos: 28, hand: null} +- {web: Digit9, xkb: ae09, windows: T0A, macos: 25, hand: null} +- {web: Digit0, xkb: ae10, windows: T0B, macos: 29, hand: null} +# Letters, first row +Letters1: +- {web: KeyQ, xkb: ad01, windows: T10, macos: 12, hand: null} +- {web: KeyW, xkb: ad02, windows: T11, macos: 13, hand: null} +- {web: KeyE, xkb: ad03, windows: T12, macos: 14, hand: null} +- {web: KeyR, xkb: ad04, windows: T13, macos: 15, hand: null} +- {web: KeyT, xkb: ad05, windows: T14, macos: 17, hand: null} +- {web: KeyY, xkb: ad06, windows: T15, macos: 16, hand: null} +- {web: KeyU, xkb: ad07, windows: T16, macos: 32, hand: null} +- {web: KeyI, xkb: ad08, windows: T17, macos: 34, hand: null} +- {web: KeyO, xkb: ad09, windows: T18, macos: 31, hand: null} +- {web: KeyP, xkb: ad10, windows: T19, macos: 35, hand: null} +# Letters, second row +Letters2: +- {web: KeyA, xkb: ac01, windows: T1E, macos: 0, hand: null} +- {web: KeyS, xkb: ac02, windows: T1F, macos: 1, hand: null} +- {web: KeyD, xkb: ac03, windows: T20, macos: 2, hand: null} +- {web: KeyF, xkb: ac04, windows: T21, macos: 3, hand: null} +- {web: KeyG, xkb: ac05, windows: T22, macos: 5, hand: null} +- {web: KeyH, xkb: ac06, windows: T23, macos: 4, hand: null} +- {web: KeyJ, xkb: ac07, windows: T24, macos: 38, hand: null} +- {web: KeyK, xkb: ac08, windows: T25, macos: 40, hand: null} +- {web: KeyL, xkb: ac09, windows: T26, macos: 37, hand: null} +- {web: Semicolon, xkb: ac10, windows: T27, macos: 41, hand: null} +# Letters, third row +Letters3: +- {web: KeyZ, xkb: ab01, windows: T2C, macos: 6, hand: null} +- {web: KeyX, xkb: ab02, windows: T2D, macos: 7, hand: null} +- {web: KeyC, xkb: ab03, windows: T2E, macos: 8, hand: null} +- {web: KeyV, xkb: ab04, windows: T2F, macos: 9, hand: null} +- {web: KeyB, xkb: ab05, windows: T30, macos: 11, hand: null} +- {web: KeyN, xkb: ab06, windows: T31, macos: 45, hand: null} +- {web: KeyM, xkb: ab07, windows: T32, macos: 46, hand: null} +- {web: Comma, xkb: ab08, windows: T33, macos: 43, hand: null} +- {web: Period, xkb: ab09, windows: T34, macos: 47, hand: null} +- {web: Slash, xkb: ab10, windows: T35, macos: 44, hand: null} +PinkyKeys: +- {web: Minus, xkb: ae11, windows: T0C, macos: 27, hand: null} +- {web: Equal, xkb: ae12, windows: T0D, macos: 24, hand: null} +- {web: IntlYen, xkb: ae13, windows: T0D, macos: 42, hand: null} +- {web: BracketLeft, xkb: ad11, windows: T1A, macos: 33, hand: null} +- {web: BracketRight, xkb: ad12, windows: T1B, macos: 30, hand: null} +- {web: Quote, xkb: ac11, windows: T28, macos: 39, hand: null} +- {web: IntlRo, xkb: ab11, windows: T28, macos: 39, hand: null} +- {web: Backquote, xkb: tlde, windows: T29, macos: 50, hand: null} +- {web: Backslash, xkb: bksl, windows: T2B, macos: 42, hand: null} +- {web: IntlBackslash, xkb: lsgt, windows: T56, macos: 10, hand: null} +SpaceBar: +- {web: Space, xkb: spce, windows: T39, macos: 49, hand: null} +Numpad: +- {web: Numpad0, xkb: kp0, windows: T52, macos: null, hand: null} +- {web: Numpad1, xkb: kp1, windows: T4F, macos: null, hand: null} +- {web: Numpad2, xkb: kp2, windows: T50, macos: null, hand: null} +- {web: Numpad3, xkb: kp3, windows: T51, macos: null, hand: null} +- {web: Numpad4, xkb: kp4, windows: T4B, macos: null, hand: null} +- {web: Numpad5, xkb: kp5, windows: T4C, macos: null, hand: null} +- {web: Numpad6, xkb: kp6, windows: T4D, macos: null, hand: null} +- {web: Numpad7, xkb: kp7, windows: T47, macos: null, hand: null} +- {web: Numpad8, xkb: kp8, windows: T48, macos: null, hand: null} +- {web: Numpad9, xkb: kp9, windows: T49, macos: null, hand: null} +- {web: NumpadEnter, xkb: kpen, windows: X1C, macos: null, hand: null} +- {web: NumpadEqual, xkb: kpeq, windows: T59, macos: null, hand: null} +- {web: NumpadDecimal, xkb: kpdl, windows: T53, macos: null, hand: null} +- {web: NumpadComma, xkb: kppt, windows: T7E, macos: null, hand: null} +- {web: NumpadDivide, xkb: kpdv, windows: X35, macos: null, hand: null} +- {web: NumpadMultipl, xkb: kpmu, windows: T37, macos: null, hand: null} +- {web: NumpadSubtrac, xkb: kpsu, windows: T4A, macos: null, hand: null} +- {web: NumpadAdd, xkb: kpad, windows: T4E, macos: null, hand: null} +- {web: NumLock, xkb: nmlk, windows: T45, macos: null, hand: null} +System: +- {web: Tab, xkb: tab, windows: T0F, macos: null, hand: Left} +- {web: Enter, xkb: rtrn, windows: T1C, macos: null, hand: null} +- {web: Backspace, xkb: bksp, windows: T0E, macos: null, hand: null} +- {web: Delete, xkb: dele, windows: X53, macos: null, hand: null} +- {web: Escape, xkb: esc, windows: T01, macos: null, hand: null} +- {web: ContextMenu, xkb: menu, windows: X5D, macos: null, hand: null} +- {web: Home, xkb: home, windows: X47, macos: null, hand: null} +- {web: End, xkb: end, windows: X4F, macos: null, hand: null} +- {web: ArrowUp, xkb: up, windows: X48, macos: null, hand: null} +- {web: ArrowDown, xkb: down, windows: X50, macos: null, hand: null} +- {web: ArrowLeft, xkb: left, windows: X4B, macos: null, hand: null} +- {web: ArrowDown, xkb: rght, windows: X50, macos: null, hand: null} +- {web: PageUp, xkb: pgup, windows: X49, macos: null, hand: null} +- {web: PageDown, xkb: pgdn, windows: X51, macos: null, hand: null} +- {web: F1, xkb: fk01, windows: T3B, macos: null, hand: null} +- {web: F2, xkb: fk02, windows: T3C, macos: null, hand: null} +- {web: F3, xkb: fk03, windows: T3D, macos: null, hand: null} +- {web: F4, xkb: fk04, windows: T3E, macos: null, hand: null} +- {web: F5, xkb: fk05, windows: T3F, macos: null, hand: null} +- {web: F6, xkb: fk06, windows: T40, macos: null, hand: null} +- {web: F7, xkb: fk07, windows: T41, macos: null, hand: null} +- {web: F8, xkb: fk08, windows: T42, macos: null, hand: null} +- {web: F9, xkb: fk09, windows: T43, macos: null, hand: null} +- {web: F10, xkb: fk10, windows: T44, macos: null, hand: null} +- {web: F11, xkb: fk11, windows: T57, macos: null, hand: null} +- {web: F12, xkb: fk12, windows: T58, macos: null, hand: null} +Modifiers: +- {web: ShiftLeft, xkb: lfsh, windows: T2A, macos: null, hand: Left} +- {web: ShiftRight, xkb: rtsh, windows: T36, macos: null, hand: Right} +- {web: CapsLock, xkb: caps, windows: T3A, macos: null, hand: Left} +- {web: AltLeft, xkb: lalt, windows: T38, macos: null, hand: Left} +- {web: AltRight, xkb: ralt, windows: X38, macos: null, hand: Right} +- {web: ControlLeft, xkb: lctl, windows: T1D, macos: null, hand: Left} +- {web: ControlRight, xkb: rctl, windows: X1D, macos: null, hand: Right} +- {web: MetaLeft, xkb: lwin, windows: X5B, macos: null, hand: Left} +- {web: MetaRight, xkb: rwin, windows: X5C, macos: null, hand: Right} +InputMethod: +- {web: NonConvert, xkb: muhe, windows: T7B, macos: null, hand: Left} +- {web: Convert, xkb: henk, windows: T79, macos: null, hand: Right} +Miscellaneous: +- {web: LaunchApp2, xkb: i148, windows: null, macos: null, hand: null} # Calculator +- {web: LaunchMail, xkb: i163, windows: null, macos: null, hand: null} +- {web: MediaPlayPause, xkb: i172, windows: X22, macos: null, hand: null} +- {web: BrowserHome, xkb: i180, windows: X32, macos: null, hand: null} diff --git a/kalamine/generators/ahk.py b/kalamine/generators/ahk.py index 47558b5..bd93025 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]: @@ -21,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: @@ -38,30 +40,37 @@ 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("") + 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 + # 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_name in ["ae13", "ab11"]: # ABNT / JIS keys - continue # these two keys are not supported yet + if key.category is not prev_category: + output.append(f"; {key.category.description}") + output.append("") + prev_category = key.category - sc = f"SC{SCAN_CODES['klc'][key_name]}" - 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_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) @@ -81,26 +90,35 @@ def ahk_shortcuts(layout: "KeyboardLayout") -> List[str]: prefixes = [" ^", "^+"] enabled = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" qwerty_vk = load_data("qwerty_vk") + layers = (Layer.BASE, Layer.SHIFT) output = [] - for key_name in LAYER_KEYS: - if key_name.startswith("-"): - output.append(f"; {key_name[1:]}") - output.append("") + 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 + # 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_name in ["ae13", "ab11"]: # ABNT / JIS keys - continue # these two keys are not supported yet + if key.category is not prev_category: + output.append(f"; {key.category.description}") + output.append("") + prev_category = key.category - scan_code = SCAN_CODES["klc"][key_name] - for i in [Layer.BASE, Layer.SHIFT]: + scan_code = key.windows[1:].lower() + for i in layers: 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] + 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/keylayout.py b/kalamine/generators/keylayout.py index f084c65..49bc1bd 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 DK_INDEX, LAYER_KEYS, SCAN_CODES, Layer, hex_ord +from ..utils import DK_INDEX, Layer, hex_ord def _xml_proof(char: str) -> str: @@ -44,35 +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.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_{DK_INDEX[key].name}" + if key.id in layer: + value = layer[key.id] + if value in layout.dead_keys: + symbol = f"dead_{DK_INDEX[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) @@ -94,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 = DK_INDEX[key].name - term = layout.dead_keys[key][key] + for dk in layout.dead_keys: + name = DK_INDEX[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: @@ -114,29 +119,33 @@ 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("-"): - ret_actions.append("") - ret_actions.append(f"") + 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"") + 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 DK_INDEX: if k in layout.dead_keys: - if key in layout.dead_keys[k]: - actions.append((DK_INDEX[k].name, layout.dead_keys[k][key])) + if value in layout.dead_keys[k]: + actions.append((DK_INDEX[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 60ff718..5890872 100644 --- a/kalamine/generators/klc.py +++ b/kalamine/generators/klc.py @@ -11,13 +11,14 @@ """ import re -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, Dict, List if TYPE_CHECKING: from ..layout import KeyboardLayout +from ..key import KEYS 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 DK_INDEX, Layer, hex_ord, load_data # return the corresponding char for a symbol @@ -46,41 +47,65 @@ def _get_langid(locale: str) -> str: oem_idx = 0 -def klc_virtual_key(layout: "KeyboardLayout", symbols: list, scan_code: str) -> str: - oem_102_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: + oem_102_scan_code = "T56" if layout.angle_mod: - oem_102_scan_code = "30" + oem_102_scan_code = "T30" if scan_code == oem_102_scan_code: # 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 = 9 # We affect abitrary OEM VK and it will not match the one @@ -103,29 +128,35 @@ 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) + virtual_keys: Dict[str, List[str]] = {} - for key_name in LAYER_KEYS: - if key_name.startswith("-"): + 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 or not key.windows.startswith("T"): + # TODO: warning continue - if key_name in ["ae13", "ab11"]: # ABNT / JIS keys - continue # these two keys are not supported yet + # 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_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][" "] 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) @@ -135,11 +166,17 @@ def klc_keymap(layout: "KeyboardLayout") -> List[str]: symbols.append("-1") description += " " + desc - scan_code = SCAN_CODES["klc"][key_name] + 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( @@ -228,27 +265,34 @@ 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 output = [] - for key_name in LAYER_KEYS: - if key_name.startswith("-"): + for key in KEYS.values(): + if key.id in ["ae13", "ab11"]: # ABNT / JIS keys + continue # these two keys are not supported yet + # TODO: add support for all scan codes + if key.windows is None or not key.windows.startswith("T"): + # TODO: warning continue - if key_name in ["ae13", "ab11"]: # ABNT / JIS keys - continue # these two keys are not supported yet + # 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_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: @@ -257,7 +301,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) @@ -268,11 +312,17 @@ 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[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/kalamine/generators/web.py b/kalamine/generators/web.py index 4c9d023..68306c2 100644 --- a/kalamine/generators/web.py +++ b/kalamine/generators/web.py @@ -12,7 +12,8 @@ if TYPE_CHECKING: from ..layout import KeyboardLayout -from ..utils import LAYER_KEYS, ODK_ID, SCAN_CODES, Layer, upper_key +from ..key import KEYS +from ..utils import ODK_ID, Layer, upper_key def raw_json(layout: "KeyboardLayout") -> Dict: @@ -21,15 +22,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("-"): + 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 @@ -90,30 +92,32 @@ 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("-"): + for key in KEYS.values(): + if key.web is None: + # TODO: warning continue - level = 0 - for i in [ - Layer.BASE, - Layer.SHIFT, - Layer.ALTGR, - Layer.ALTGR_SHIFT, - Layer.ODK, - Layer.ODK_SHIFT, - ]: - level += 1 - if key_name not in layout.layers[i]: + for level, i in enumerate( + ( + Layer.BASE, + Layer.SHIFT, + Layer.ALTGR, + Layer.ALTGR_SHIFT, + Layer.ODK, + Layer.ODK_SHIFT, + ), + start=1, + ): + 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 3ef688e..f2d7871 100644 --- a/kalamine/generators/xkb.py +++ b/kalamine/generators/xkb.py @@ -4,13 +4,14 @@ - xkb symbols/patch for XOrg (system-wide) & Wayland (system-wide/user-space) """ -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional if TYPE_CHECKING: from ..layout import KeyboardLayout +from ..key import KEYS, KeyCategory 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, ODK_ID, hex_ord, load_data XKB_KEY_SYM = load_data("key_sym") @@ -27,18 +28,14 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False) -> List[str]: max_length = 16 # `ISO_Level3_Latch` should be the longest symbol name output: List[str] = [] - for key_name in LAYER_KEYS: - if key_name.startswith("-"): # separator - if output: - output.append("") - output.append("//" + key_name[1:]) - continue - + prev_category: Optional[KeyCategory] = None + for key in KEYS.values(): descs = [] symbols = [] - for layer in layout.layers.values(): - if key_name in layer: - keysym = layer[key_name] + has_symbols = False + for keys in layout.layers.values(): + if key.id in keys: + keysym = keys[key.id] desc = keysym # dead key? if keysym in DK_INDEX: @@ -50,6 +47,7 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False) -> List[str]: symbol = XKB_KEY_SYM[keysym] else: symbol = f"U{hex_ord(keysym).upper()}" + has_symbols = True else: desc = " " symbol = "VoidSymbol" @@ -57,17 +55,26 @@ def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False) -> List[str]: descs.append(desc) symbols.append(symbol.ljust(max_length)) - key = "{{[ {0}, {1}, {2}, {3}]}}" # 4-level layout by default + if not has_symbols: + continue + + if key.category is not prev_category: + if output: + output.append("") + output.append("// " + key.category.description) + prev_category = key.category + + key_template = "{{[ {0}, {1}, {2}, {3}]}}" # 4-level layout by default description = "{0} {1} {2} {3}" if 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: # 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] @@ -75,7 +82,8 @@ 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)};" + 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("\\"): diff --git a/kalamine/help.py b/kalamine/help.py index 38c0d39..c9e4430 100644 --- a/kalamine/help.py +++ b/kalamine/help.py @@ -1,8 +1,8 @@ from pathlib import Path from typing import Dict, List +from .key import KEYS from .layout import KeyboardLayout -from .template import SCAN_CODES from .utils import Layer, load_data SEPARATOR = ( @@ -84,8 +84,8 @@ def dummy_layout( descriptor["base"] = descriptor.pop("1dk") # XXX this should be a dataclass - for key, val in meta.items(): - descriptor[key] = val + for d, val in meta.items(): + descriptor[d] = val # make a KeyboardLayout matching the input parameters descriptor["geometry"] = "ANSI" # layout.yaml has an ANSI geometry @@ -93,10 +93,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..6b1ce7e --- /dev/null +++ b/kalamine/key.py @@ -0,0 +1,112 @@ +from dataclasses import dataclass +from enum import Enum, Flag, auto, unique +from typing import Any, Dict, Optional + +from kalamine.utils import load_data + + +@unique +class Hand(Enum): + Left = auto() + Right = auto() + + @classmethod + def parse(cls, raw: str) -> "Hand": + 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() + Numpad = auto() + System = auto() + Modifiers = auto() + InputMethod = auto() + Miscellaneous = auto() + AlphaNum = Digits | Letters1 | Letters2 | Letters3 | PinkyKeys | SpaceBar + + @classmethod + def parse(cls, raw: str) -> "KeyCategory": + for kc in cls: + 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: + 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 +class Key: + xkb: str + web: Optional[str] = None + windows: Optional[str] = None + macos: Optional[str] = None + hand: Optional[Hand] = None + "Usual hand on standard (ISO, etc.) keyboard" + category: KeyCategory = KeyCategory.Miscellaneous + + @classmethod + def parse( + cls, + category: str, + xkb: str, + web: Optional[str], + windows: Optional[str], + macos: Optional[str], + hand: Optional[str], + ) -> "Key": + return cls( + category=KeyCategory.parse(category), + xkb=xkb, + web=web, + windows=windows, + macos=macos, + hand=Hand.parse(hand) if hand else None, + ) + + @classmethod + def parse_keys(cls, data: Dict[str, Any]) -> Dict[str, "Key"]: + return { + key.xkb: key + for category, keys in data.items() + for key in (cls.parse(category=category, **entry) for entry in keys) + } + + @property + def id(self) -> str: + return self.xkb + + @property + def alphanum(self) -> bool: + return bool(self.category & KeyCategory.AlphaNum) + + +KEYS = Key.parse_keys(load_data("scan_codes")) diff --git a/kalamine/layout.py b/kalamine/layout.py index c7cfc11..7c6c0fa 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -2,21 +2,14 @@ import sys from dataclasses import dataclass from pathlib import Path -from typing import Dict, List, Optional, Set, Type, TypeVar +from typing import Dict, List, Optional, Set, Type, TypeVar, Union import click import tomli import yaml -from .utils import ( - DEAD_KEYS, - LAYER_KEYS, - ODK_ID, - Layer, - load_data, - text_to_lines, - upper_key, -) +from .key import KEYS +from .utils import DEAD_KEYS, ODK_ID, Layer, load_data, text_to_lines, upper_key ### # Helpers @@ -208,8 +201,47 @@ 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_extra_mapping(mapping) + self._parse_dead_keys(spc) + @staticmethod + def _parse_key_ref(raw: str) -> Optional[str]: + """Parse a key reference (e.g. to clone)""" + 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, Union[str, Dict[str, str]]]): + """Parse a layout dict""" + layer: Optional[Layer] + for raw_key, levels in mapping.items(): + # TODO: parse key in various ways (XKB, Linux keycode) + 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 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(): + 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: + value = raw_value + self.layers[layer][key] = value + def _parse_dead_keys(self, spc: Dict[str, str]) -> None: """Build a deadkey dict.""" @@ -241,9 +273,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..b1d4d7a 100644 --- a/kalamine/template.py +++ b/kalamine/template.py @@ -3,15 +3,12 @@ import re from typing import TYPE_CHECKING, List -from .utils import lines_to_text, load_data +from .utils import lines_to_text if TYPE_CHECKING: 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 d19967e..ee49dfc 100644 --- a/kalamine/utils.py +++ b/kalamine/utils.py @@ -47,15 +47,37 @@ class Layer(IntEnum): ALTGR = 4 ALTGR_SHIFT = 5 + @classmethod + def parse(cls, raw: str) -> Optional["Layer"]: + rawʹ = raw.casefold() + # Parse alternate names + if rawʹ == "1dk": + return cls(cls.ODK) + elif rawʹ == "1dk_shift": + return cls(cls.ODK_SHIFT) + # Parse native values + else: + for layer in cls: + # Parse native names + if rawʹ == layer.name.casefold(): + return layer + # Parse numeric values + try: + if int(raw, base=10) == layer.value: + return layer + except ValueError: + pass + return None + def next(self) -> "Layer": """The next layer in the layer ordering.""" return Layer(int(self) + 1) 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 @@ -105,66 +127,4 @@ class DeadKeyDescr: 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 - -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", -] diff --git a/tests/test_serializer_klc.py b/tests/test_serializer_klc.py index ce916ae..4691208 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,34 @@ def test_intl_keymap(): 39 SPACE 0 0020 0020 -1 -1 // """ ) + assert len(keymap) == len(keymap_ref) + assert keymap == keymap_ref + + # Extra mapping section + extraMapping = { + # Redefine level of key previously defined in ASCII art + "ae01": {"shift": "?"}, + # TODO + # Test layer case variants and ODK alias + # "kppt": {"base": ",", "sHiFt": ";", "1dk": ".", "ODk_shiFt": ":"}, + # Clone level of another key previously defined in ASCII art + "esc": {"base": "\x1b", "shift": "(ae11)"}, + # Clone whole key previously defined + "henk": "(lsgt)", + } + + # Resulting klc keymap + 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(): diff --git a/tests/test_serializer_xkb.py b/tests/test_serializer_xkb.py index 302e4a6..69c2fba 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( """ @@ -70,14 +73,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 ]}; // ' ' @@ -94,8 +94,6 @@ def test_ansi(): def test_intl(): - layout = load_layout("intl") - expected = split( """ // Digits @@ -149,11 +147,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 ]}; // \\ | @@ -163,17 +159,47 @@ def test_intl(): """ ) - xkbcomp = xkb_table(layout, xkbcomp=True) - assert len(xkbcomp) == len(expected) - assert xkbcomp == expected + # Extra mapping section + extraMapping = { + # Redefine level of key previously defined in ASCII art + "ae01": {"shift": "?"}, + # Test layer case variants and ODK alias + "menu": {"base": "a", "sHiFt": "A", "1dk": "æ", "ODk_shiFt": "Æ"}, + # Clone level of another key previously defined in ASCII art + "esc": {"base": "(ae11)"}, + # Clone whole key previously defined + "i172": "(lsgt)", + } + + # Extra mapping keymap + 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( """ @@ -228,14 +254,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 ]}; // 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