331 lines
11 KiB
Python
331 lines
11 KiB
Python
|
import base64
|
||
|
|
||
|
from typing import TypeVar, List
|
||
|
|
||
|
from xml.dom import minidom
|
||
|
from xml.parsers.expat import ExpatError
|
||
|
from xml.dom.minidom import Document, Node, Element, getDOMImplementation
|
||
|
|
||
|
from kc import KeyCode
|
||
|
from mackb import StateName, ActionID, MapIndex, Modifier, ModMap, \
|
||
|
State, KeyMap, OnPress, ActionReference, Output, EnterState, Action, \
|
||
|
MacKeyboardLayout
|
||
|
|
||
|
# Ukelele/mac uses ꯍ for escaping "invalid" characters, but Python puts the char directly. We search for all strings in the input, decode them and base64 encode them so we can parse them later
|
||
|
def encode_document(inp: str) -> str:
|
||
|
in_str = False
|
||
|
# TODO: This should be a bytes object. The input seems to be utf-16
|
||
|
# However, I don't think we'll be parsing anything outside 0x0-0xffff
|
||
|
out = ""
|
||
|
string_contents = ""
|
||
|
i = 0
|
||
|
inside_proper_tag = False
|
||
|
while i < len(inp):
|
||
|
ch = inp[i]
|
||
|
if not in_str:
|
||
|
if ch == "<":
|
||
|
inside_proper_tag = inp[i + 1] not in "?!"
|
||
|
if ch == '"':
|
||
|
in_str = True
|
||
|
else:
|
||
|
out += ch
|
||
|
i += 1
|
||
|
else:
|
||
|
if ch == "&" and inp[i + 1] == "#" and inp[i + 2] == "x" and inp[i+7] == ";":
|
||
|
hexed = inp[i+3:i+7]
|
||
|
string_contents += chr(int(hexed, 16))
|
||
|
i += 8
|
||
|
elif ch == '"':
|
||
|
if inside_proper_tag:
|
||
|
out += '"' + base64.b64encode(string_contents.encode()).decode() + '"'
|
||
|
else:
|
||
|
out += '"' + string_contents + '"'
|
||
|
string_contents = ""
|
||
|
in_str = False
|
||
|
i += 1
|
||
|
else:
|
||
|
string_contents += ch
|
||
|
i += 1
|
||
|
return out
|
||
|
|
||
|
def percent_encode(inp: str) -> str:
|
||
|
out = ""
|
||
|
for ch in inp:
|
||
|
if ord(ch) < 0x20 or ch in '&"<>':
|
||
|
out += "&#x" + hex(ord(ch))[2:].rjust(4, '0') + ";"
|
||
|
else:
|
||
|
out += ch
|
||
|
return out
|
||
|
|
||
|
def decode_document(inp: str) -> str:
|
||
|
in_str = False
|
||
|
out = ""
|
||
|
string_contents = ""
|
||
|
i = 0
|
||
|
inside_proper_tag = False
|
||
|
while i < len(inp):
|
||
|
ch = inp[i]
|
||
|
if not in_str:
|
||
|
if ch == "<":
|
||
|
inside_proper_tag = inp[i + 1] not in "?!"
|
||
|
if ch == '"':
|
||
|
in_str = True
|
||
|
else:
|
||
|
out += ch
|
||
|
i += 1
|
||
|
else:
|
||
|
if ch == '"':
|
||
|
if inside_proper_tag:
|
||
|
out += '"' + percent_encode(base64.b64decode(string_contents.encode()).decode()) + '"'
|
||
|
else:
|
||
|
out += '"' + string_contents + '"'
|
||
|
string_contents = ""
|
||
|
in_str = False
|
||
|
i += 1
|
||
|
else:
|
||
|
string_contents += ch
|
||
|
i += 1
|
||
|
return out
|
||
|
|
||
|
def decode_tree(d: Document) -> Document:
|
||
|
def recode_in_place(n: Node):
|
||
|
if isinstance(n, Element):
|
||
|
for name, value in n.attributes.items():
|
||
|
n.setAttribute(name, base64.b64decode(value.encode()).decode())
|
||
|
|
||
|
for child in n.childNodes:
|
||
|
recode_in_place(child)
|
||
|
for ch in d.childNodes:
|
||
|
recode_in_place(ch)
|
||
|
return d
|
||
|
|
||
|
def encode_tree(d: Document) -> Document:
|
||
|
def recode_in_place(n: Node):
|
||
|
if isinstance(n, Element):
|
||
|
for name, value in n.attributes.items():
|
||
|
n.setAttribute(name, base64.b64encode(value.encode()).decode())
|
||
|
|
||
|
for child in n.childNodes:
|
||
|
recode_in_place(child)
|
||
|
for ch in d.childNodes:
|
||
|
recode_in_place(ch)
|
||
|
return d
|
||
|
|
||
|
def load_file(path: str) -> Document:
|
||
|
with open(path, "r") as f:
|
||
|
data = f.read()
|
||
|
|
||
|
parsed = encode_document(data)
|
||
|
with open("/tmp/mjau-i", "w") as o:
|
||
|
o.write(parsed)
|
||
|
|
||
|
return decode_tree(minidom.parseString(parsed))
|
||
|
|
||
|
def save_file(path: str, x: Document):
|
||
|
gen = encode_tree(x).toprettyxml(indent=" ")
|
||
|
|
||
|
with open("/tmp/mjau-o", "w") as o:
|
||
|
o.write(gen)
|
||
|
|
||
|
with open(path, "w") as out:
|
||
|
out.write(decode_document(gen))
|
||
|
|
||
|
A = TypeVar("A")
|
||
|
def get_only(l: List[A], name: str) -> A:
|
||
|
if l == []:
|
||
|
raise ValueError(f"No {name}")
|
||
|
if len(l) > 1:
|
||
|
raise ValueError(f"Too many {name} ({len(l)})")
|
||
|
return l[0]
|
||
|
|
||
|
def parse_modmap(node: Node) -> ModMap:
|
||
|
used_keys = []
|
||
|
selects = {}
|
||
|
for select in node.childNodes:
|
||
|
if select.nodeName != "keyMapSelect":
|
||
|
continue
|
||
|
map_index = MapIndex(int(select.attributes["mapIndex"].value))
|
||
|
modifier = get_only([ch for ch in select.childNodes if ch.nodeName == "modifier"], f"modifier of {map_index}")
|
||
|
key_names = modifier.attributes["keys"].value.split(" ")
|
||
|
modifier_keys = set()
|
||
|
for key_name in key_names:
|
||
|
try:
|
||
|
modifier_keys.add(Modifier.from_name(key_name))
|
||
|
except ValueError as e:
|
||
|
continue
|
||
|
if modifier_keys in used_keys:
|
||
|
print(f"modifier select {map_index} already used, skipping")
|
||
|
continue
|
||
|
used_keys.append(modifier_keys)
|
||
|
|
||
|
selects[map_index] = modifier_keys
|
||
|
|
||
|
return ModMap(selects)
|
||
|
|
||
|
def parse_onpress(node: Element) -> OnPress:
|
||
|
if "action" in node.attributes:
|
||
|
return ActionReference(node.attributes["action"].value)
|
||
|
if "output" in node.attributes:
|
||
|
return Output(node.attributes["output"].value)
|
||
|
if "next" in node.attributes:
|
||
|
return EnterState(node.attributes["next"].value)
|
||
|
raise ValueError(f"node {node} with attributes {dict(node.attributes)} has no on-press")
|
||
|
|
||
|
def parse_keymap(node: Node) -> KeyMap:
|
||
|
keys = {}
|
||
|
for key in node.childNodes:
|
||
|
if key.nodeName != "key":
|
||
|
continue
|
||
|
|
||
|
code = KeyCode(int(key.attributes["code"].value))
|
||
|
on_press = parse_onpress(key)
|
||
|
|
||
|
keys[code] = on_press
|
||
|
return KeyMap(keys)
|
||
|
|
||
|
def parse_keyboard_layout(path: str) -> MacKeyboardLayout:
|
||
|
dom: Document = load_file(path)
|
||
|
|
||
|
keyboard = get_only(dom.getElementsByTagName("keyboard"), "keyboard")
|
||
|
name = keyboard.attributes["name"].value
|
||
|
|
||
|
states_node = get_only(dom.getElementsByTagName("terminators"), "terminators")
|
||
|
|
||
|
modmap_node = get_only(dom.getElementsByTagName("modifierMap"), "modifierMap")
|
||
|
keymap_nodes = dom.getElementsByTagName("keyMapSet")
|
||
|
ansi_keymaps = [keymap_node for keymap_node in keymap_nodes if keymap_node.attributes["id"].value == "ANSI"]
|
||
|
keymapset_node = get_only(ansi_keymaps, "keymapSet (ANSI)")
|
||
|
|
||
|
actions_node = get_only(dom.getElementsByTagName("actions"), "actions")
|
||
|
|
||
|
states = {}
|
||
|
for state_node in states_node.childNodes:
|
||
|
if state_node.nodeName != "when":
|
||
|
continue
|
||
|
|
||
|
state_name = StateName(state_node.attributes["state"].value)
|
||
|
terminator = state_node.attributes["output"].value
|
||
|
states[state_name] = State(terminator)
|
||
|
|
||
|
modmap = parse_modmap(modmap_node)
|
||
|
keymaps = {}
|
||
|
for keymap_node in keymapset_node.childNodes:
|
||
|
if keymap_node.nodeName != "keyMap":
|
||
|
continue
|
||
|
|
||
|
map_index = MapIndex(int(keymap_node.attributes["index"].value))
|
||
|
keymap = parse_keymap(keymap_node)
|
||
|
keymaps[map_index] = keymap
|
||
|
|
||
|
actions = {}
|
||
|
for action in actions_node.childNodes:
|
||
|
if action.nodeName != "action":
|
||
|
continue
|
||
|
action_id = ActionID(action.attributes["id"].value)
|
||
|
state_actions = {}
|
||
|
for on_press_node in action.childNodes:
|
||
|
if on_press_node.nodeName != "when":
|
||
|
continue
|
||
|
state = StateName(on_press_node.attributes["state"].value)
|
||
|
on_press = parse_onpress(on_press_node)
|
||
|
|
||
|
state_actions[state] = on_press
|
||
|
|
||
|
actions[action_id] = Action(state_actions)
|
||
|
|
||
|
terminators_node = get_only(dom.getElementsByTagName("terminators"), "terminators")
|
||
|
|
||
|
for terminator in terminators_node.childNodes:
|
||
|
if terminator.nodeName != "when":
|
||
|
continue
|
||
|
output = terminator.attributes["output"].value
|
||
|
state = StateName(output)
|
||
|
|
||
|
return MacKeyboardLayout(
|
||
|
name=name,
|
||
|
states=states,
|
||
|
modmap=modmap,
|
||
|
keymaps=keymaps,
|
||
|
actions=actions,
|
||
|
)
|
||
|
|
||
|
def write_on_press(doc: Document, name: str, on_press: OnPress) -> Element:
|
||
|
key = doc.createElement(name)
|
||
|
|
||
|
if isinstance(on_press, ActionReference):
|
||
|
key.setAttribute("action", on_press.ref)
|
||
|
elif isinstance(on_press, Output):
|
||
|
key.setAttribute("output", on_press.output)
|
||
|
elif isinstance(on_press, EnterState):
|
||
|
key.setAttribute("next", on_press.state_name)
|
||
|
else:
|
||
|
raise ValueError(on_press)
|
||
|
|
||
|
return key
|
||
|
|
||
|
def unparse(kb: MacKeyboardLayout) -> Document:
|
||
|
impl = getDOMImplementation()
|
||
|
assert impl is not None
|
||
|
|
||
|
maxout = 0
|
||
|
for ac in kb.actions.values():
|
||
|
for onpress in ac.state_actions.values():
|
||
|
if isinstance(onpress, Output):
|
||
|
maxout = max(maxout, len(onpress.output.encode("utf-16-le")) // 2)
|
||
|
for km in kb.keymaps.values():
|
||
|
for onpress in km.keys.values():
|
||
|
if isinstance(onpress, Output):
|
||
|
maxout = max(maxout, len(onpress.output.encode("utf-16-le")) // 2)
|
||
|
|
||
|
doc = impl.createDocument(None, "keyboard", None)
|
||
|
keyboard = doc.documentElement
|
||
|
keyboard.setAttribute("group", str(126))
|
||
|
keyboard.setAttribute("id", str(123456))
|
||
|
keyboard.setAttribute("name", kb.name)
|
||
|
keyboard.setAttribute("maxout", str(maxout))
|
||
|
|
||
|
keyboard.appendChild(layouts := doc.createElement("layouts"))
|
||
|
layouts.appendChild(layout := doc.createElement("layout"))
|
||
|
layout.setAttribute("first", str(0))
|
||
|
layout.setAttribute("last", str(207))
|
||
|
layout.setAttribute("mapSet", "ANSI")
|
||
|
layout.setAttribute("modifiers", "Modifiers")
|
||
|
|
||
|
keyboard.appendChild(modifierMap := doc.createElement("modifierMap"))
|
||
|
modifierMap.setAttribute("id", "Modifiers")
|
||
|
modifierMap.setAttribute("defaultIndex", str(0))
|
||
|
|
||
|
for idx, map in kb.modmap.selects.items():
|
||
|
modifierMap.appendChild(kbsel := doc.createElement("keyMapSelect"))
|
||
|
kbsel.setAttribute("mapIndex", str(idx))
|
||
|
kbsel.appendChild(modifier := doc.createElement("modifier"))
|
||
|
modifier.setAttribute("keys", " ".join(m.value for m in map))
|
||
|
|
||
|
keyboard.appendChild(keymapset := doc.createElement("keyMapSet"))
|
||
|
keymapset.setAttribute("id", "ANSI")
|
||
|
for idx, km in kb.keymaps.items():
|
||
|
keymapset.appendChild(keymap := doc.createElement("keyMap"))
|
||
|
keymap.setAttribute("index", str(idx))
|
||
|
|
||
|
for kc, on_press in km.keys.items():
|
||
|
keymap.appendChild(doc.createComment(str(kc)))
|
||
|
keymap.appendChild(key := write_on_press(doc, "key", on_press))
|
||
|
key.setAttribute("code", str(kc.kc))
|
||
|
|
||
|
keyboard.appendChild(actions := doc.createElement("actions"))
|
||
|
for action_id, action in kb.actions.items():
|
||
|
actions.appendChild(action_elem := doc.createElement("action"))
|
||
|
action_elem.setAttribute("id", action_id)
|
||
|
|
||
|
for state_name, on_press in action.state_actions.items():
|
||
|
action_elem.appendChild(when_elem := write_on_press(doc, "when", on_press))
|
||
|
when_elem.setAttribute("state", state_name)
|
||
|
|
||
|
keyboard.appendChild(terminators := doc.createElement("terminators"))
|
||
|
for state_name, state in kb.states.items():
|
||
|
terminators.appendChild(terminator := doc.createElement("when"))
|
||
|
terminator.setAttribute("state", state_name)
|
||
|
terminator.setAttribute("output", state.terminator)
|
||
|
|
||
|
return doc
|