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