# "native" representation of a keyboard layout from __future__ import annotations from typing import Dict, List, NewType, Set, Any, Optional, TYPE_CHECKING from abc import ABC from enum import Enum if TYPE_CHECKING: import mackb from kc import KeyCode LayerName = NewType("LayerName", str) class Modifier(Enum): SHIFT = "anyShift" OPTION = "anyOption" @staticmethod def from_name(name: str) -> Modifier: for mod in Modifier: if mod.value == name: return mod raise ValueError(f"No such modifier {name}") def shorthand(self) -> str: if self == Modifier.SHIFT: return "S" elif self == Modifier.OPTION: return "A" raise ValueError(self) def __lt__(self, other: Modifier) -> bool: return self.value < other.value class Key: def __init__(self, keycode: KeyCode, modifiers: Set[Modifier]): self.keycode = keycode self.modifiers = modifiers def __str__(self) -> str: return "-".join([*[m.shorthand() for m in self.modifiers], f"{self.keycode}"]) def __eq__(self, other: Any) -> bool: if isinstance(other, Key): return self.keycode == other.keycode and self.modifiers == other.modifiers return False def __hash__(self) -> int: return hash((self.keycode, tuple(sorted(list(self.modifiers))))) ^ 0xE621 class ButtonPress(ABC): pass class Output(ButtonPress): def __init__(self, output: str): self.output = output def __repr__(self) -> str: return f"Output({self.output!r})" class ChangeLayer(ButtonPress): def __init__(self, layer: LayerName): self.layer = layer def __repr__(self) -> str: return f"ChangeLayer({self.layer!r})" class Layer: def __init__( self, terminator: Optional[str], buttons: Dict[Key, ButtonPress], ): self.terminator = terminator self.buttons = buttons class KeyboardLayout: def __init__( self, name: str, default_layer: LayerName, layers: Dict[LayerName, Layer], ): self.name = name self.default_layer = default_layer self.layers = layers def from_mac(mac: mackb.MacKeyboardLayout) -> KeyboardLayout: import mackb layers = {} for (name, state) in mac.states.items(): layers[LayerName(name)] = Layer(terminator=state.terminator, buttons={}) layers[LayerName("none")] = Layer(terminator=None, buttons={}) def resolve_action(o: mackb.OnPress, current_state: mackb.StateName) -> Optional[ButtonPress]: visited: List[mackb.ActionID] = [] while True: if isinstance(o, mackb.ActionReference): if o.ref in visited: print("Cycle detected!", visited) exit(1) visited.append(o.ref) state_actions = mac.actions[o.ref].state_actions if current_state in state_actions: o = state_actions[current_state] else: return None elif isinstance(o, mackb.Output): return Output(o.output) elif isinstance(o, mackb.EnterState): return ChangeLayer(LayerName(o.state_name)) else: raise ValueError(o) for state_name in list(mac.states.keys()) + [mackb.StateName("none")]: for idx, modifiers in mac.modmap.selects.items(): for code, on_press in mac.keymaps[idx].keys.items(): action = resolve_action(on_press, state_name) if action is None: continue if isinstance(action, Output) and action.output == "": continue # ignore empty keys key = Key(code, modifiers) layers[LayerName(state_name)].buttons[key] = action return KeyboardLayout(mac.name, LayerName("none"), layers) if __name__ == "__main__": import mac_parser mac_kb = mac_parser.parse_keyboard_layout("../svorak.new.keylayout") kb = from_mac(mac_kb) print(kb) for name, layer in kb.layers.items(): print(f"Layer {name} (terminator={layer.terminator!r}):") for key, press in layer.buttons.items(): print(f" {str(key):>20} -> {press}")