commit 90e336b668c2f8dd0c650f61301c7f38cdd5fb39 Author: xenia Date: Thu Nov 9 14:16:16 2023 +0100 first commit diff --git a/kbtrans/agda_input_method.py b/kbtrans/agda_input_method.py new file mode 100644 index 0000000..759dd72 --- /dev/null +++ b/kbtrans/agda_input_method.py @@ -0,0 +1,247 @@ +from typing import List, Tuple, Optional + +import random +import argparse +import sys +import os + +from mackb import StateName, ActionID, MapIndex, Modifier, ModMap, \ + State, KeyMap, OnPress, ActionReference, Output, EnterState, Action, \ + MacKeyboardLayout + +import agda_list +import mac_parser +import mackb +import kc + + +def resolve_onpress(kb: mac_parser.MacKeyboardLayout, p: mackb.OnPress) -> Optional[mackb.Output]: + if isinstance(p, mackb.Output): + return p + elif isinstance(p, mackb.ActionReference): + action = kb.actions[p.ref] + if not mackb.NO_STATE in action.state_actions: + return None + return resolve_onpress(kb, kb.actions[p.ref].state_actions[mackb.NO_STATE]) + return None + + +def where_is(kb: mac_parser.MacKeyboardLayout, ch: str) -> Optional[Tuple[mackb.MapIndex, kc.KeyCode]]: + for map_index in kb.modmap.selects.keys(): + for key_code, action in kb.keymaps[map_index].keys.items(): + res = resolve_onpress(kb, action) + if isinstance(res, mackb.Output) and res.output == ch: + return map_index, key_code + return None + +def encode_char(ch: str) -> str: + if 'a' <= ch <= 'z' or 'A' <= ch <= 'Z' or '0' <= ch <= '9': + return ch + return "_" + hex(ord(ch))[2:] + "_" + +def prefix_state_name(prefix: str) -> mackb.StateName: + return mackb.StateName("agda-" + mackb.StateName("".join(map(encode_char, prefix)))) + +ERROR = -1 +INFO = 0 +DEBUG = 1 + +class Agdifier: + def __init__( + self, + entry_keycode: kc.KeyCode, + terminator: str, + unknown_replacement: str, + ): + self.entry_keycode = entry_keycode + self.terminator = terminator + self.unknown_replacement = unknown_replacement + + self._last_state = 0 + + def log(self, level: int, *text): + if level == ERROR: + print("[agdifier] (ERROR)", *text, file=sys.stderr) + if level == INFO: + print("[agdifier] (INFO)", *text, file=sys.stderr) + #if level == DEBUG: + # print("[agdifier] (DEBUG)", *text, file=sys.stderr) + + def make_new_id(self, prefix: str) -> str: + self._last_state += 1 + return f"{prefix}-{self._last_state}" + + def new_action(self, kb: mac_parser.MacKeyboardLayout) -> mackb.ActionID: + action_id = mackb.ActionID(self.make_new_id("a")) + kb.actions[action_id] = mackb.Action({}) + return action_id + + def add_state_if_needed( + self, + kb: mac_parser.MacKeyboardLayout, + state_name: mackb.StateName, + terminator: str, + ): + if state_name in kb.states: + return + self.log(DEBUG, f"Adding state {state_name!r}") + kb.states[state_name] = mackb.State(terminator=terminator) + + # Ensures that all chars in the sequence has a key that makes it, and that key has an ActionReference + # Returns false if keybind cannot be added + def ensure_keybind( + self, + kb: mac_parser.MacKeyboardLayout, + keybind: str, + ) -> bool: + self.log(DEBUG, f"Verifying {keybind!r}") + for ch in keybind: + at = where_is(kb, ch) + if at is None: + self.log(ERROR, f"Keybind {keybind!r} cannot be added: missing char on base layer: {ch!r}") + return False + modmapidx, keycode = at + key_action = kb.keymaps[modmapidx].keys[keycode] + if isinstance(key_action, mackb.EnterState): + self.log(ERROR, f"Keybind {keybind!r} cannot be added: {ch!r} performs an unconditional EnterState") + return False + + if isinstance(key_action, mackb.Output): + self.log(DEBUG, f"Key {kb.modmap.selects[modmapidx]}-{keycode} does not enter action (gives {key_action}). Adding new action.") + action_id = self.new_action(kb) + kb.actions[action_id].state_actions[mackb.NO_STATE] = key_action + for state_name in kb.states.keys(): + kb.actions[action_id].state_actions[state_name] = key_action + kb.keymaps[modmapidx].keys[keycode] = mackb.ActionReference(action_id) + + return True + + def add_word( + self, + kb: mac_parser.MacKeyboardLayout, + initial_agda_state: mackb.StateName, + keybind: str, + is_prefix: bool, + target: str + ): + self.log(INFO, f"Adding keybind {keybind!r} -> {target!r} (is_prefix = {is_prefix})") + + # Create the states + for i in range(len(keybind) + is_prefix): + prefix = keybind[:i] + state_name = prefix_state_name(prefix) + if is_prefix and i == len(keybind): + self.add_state_if_needed(kb, state_name, target) + else: + self.add_state_if_needed(kb, state_name, prefix) + + for i in range(len(keybind)): + prefix = keybind[:i] + next_ch = keybind[i] + + current_state = prefix_state_name(prefix) if i > 0 else initial_agda_state + + modmapidx, keycode = where_is(kb, next_ch) # type: ignore # We know this is not None + key_action = kb.keymaps[modmapidx].keys[keycode] + assert isinstance(key_action, mackb.ActionReference) + action = kb.actions[key_action.ref] + + if i == len(keybind) - 1 and not is_prefix: + # last key, output the target + action.state_actions[current_state] = mackb.Output(target) + else: + next_state = prefix_state_name(prefix + next_ch) + action.state_actions[current_state] = mackb.EnterState(next_state) + + + def agdify(self, kb: mac_parser.MacKeyboardLayout) -> mac_parser.MacKeyboardLayout: + initial_agda_state = mackb.StateName("agda") + kb.states[initial_agda_state] = mackb.State(terminator=self.terminator) + + initial_enter = self.new_action(kb) + kb.actions[initial_enter].state_actions[mackb.NO_STATE] = mackb.EnterState(initial_agda_state) + + kb.keymaps[mackb.MapIndex(0)].keys[self.entry_keycode] = mackb.ActionReference(initial_enter) + + working_words: List[Tuple[str, str]] = [] + for keybind, target in agda_list.agda_words: + if self.ensure_keybind(kb, keybind): + working_words.append((keybind, target)) + + # Start with short words + working_words.sort(key=lambda x: x[0]) + working_words.sort(key=lambda x: len(x[0])) + + for i, (keybind, target) in enumerate(working_words): + is_prefix = any(x.startswith(keybind) for x, _ in working_words[i+1:]) + self.add_word(kb, initial_agda_state, keybind, is_prefix, target) + + return kb + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + prog="agda_input_method.py", + description="Add an Agda layer to a layout", + ) + + parser.add_argument( + "input_path", + ) + parser.add_argument( + "output_path", + ) + parser.add_argument( + "--entry-keycode", + type=int, + default=39, # svorak ! + ) + parser.add_argument( + "--terminator", + type=str, + default="🐔", + ) + parser.add_argument( + "--unknown", + type=str, + default="🐈", + ) + parser.add_argument( + "--name", + help="What do name the output keyboard. Default is to add a \" — Agda\" to the name of the input keyboard", + type=str, + required=False + ) + + opts = parser.parse_args() + + def eprint(*text): + print(*text, file=sys.stderr) + + if not os.path.isfile(opts.input_path): + eprint(f"Input file does not exist: {opts.input_path}") + exit(1) + + if not os.path.isdir(os.path.dirname(opts.output_path)): + eprint(f"Output folder does not exist: {os.path.dirname(opts.output_path)}") + exit(1) + + agdifier = Agdifier( + entry_keycode=kc.KeyCode(opts.entry_keycode), + terminator=opts.terminator, + unknown_replacement=opts.unknown + ) + + eprint("Loading keyboard layout") + kb = mac_parser.parse_keyboard_layout(opts.input_path) + + eprint("Agdifying") + agdified = agdifier.agdify(kb) + + if opts.name: + agdified.name = opts.name + else: + agdified.name = agdified.name + " — Agda" + + eprint("Saving") + mac_parser.save_file(opts.output_path, mac_parser.unparse(kb)) + eprint("done") diff --git a/kbtrans/agda_list.py b/kbtrans/agda_list.py new file mode 100644 index 0000000..88731fc --- /dev/null +++ b/kbtrans/agda_list.py @@ -0,0 +1,286 @@ +import re +from typing import List, Tuple + +extras: List[Tuple[str, str]] = [ + ("cat", "🐱"), + ("cat2", "🐈"), + ("pl", "🥺"), + ("plea", "🥺"), + ("pleading", "🥺"), + ("fl", "😳"), + ("flushed", "😳"), + ("crab", "🦀"), + ("crazy hamburger", "🍔"), + ("oil from iraq", "🛢️"), + ("bread from turkie", "🍞"), + ("cake", "🍰"), + ("gec", "🦎"), + ("amongus", "ඩ"), + ("enby", "⚥"), + ("enby1", "⚧"), + ("enby2", "☄"), + ("enby3", "☿"), + ("dot", "·"), + ("bullet", "∙"), + ("multiocularo", "ꙮ"), + ("pleadpoint", "🥺👉👈"), + ("point", "👉👈"), + ("inv", "⁻¹"), +] + +def parse_source(x: str) -> List[Tuple[str, str]]: + out = [] + singular = re.compile(r'!(\S+)\s+(\S)\s+') + for k, v in singular.findall(x): + if '\\' in k or ("{" in k and "}" in k): + continue + if ord(v) < 0xffff: + out.append((k, v)) + + return out + +source = r""" +!   !FB B !bR ℝ !BGh 𝛈 !MiA 𝐴 !"{I} Ï !c{h} ḩ !amalg ∐ !lambda λ !^\script g ᶢ +!% % !FC C !bS 𝕊 !BGi 𝛊 !MiB 𝐵 !"{O} Ö !c{k} ķ !aoint ∳ !langle ⟨ !^\turned a ᵄ +!+ + !FD D !bT 𝕋 !BGk 𝛋 !MiC 𝐶 !"{U} Ü !c{l} ļ !asymp ≍ !lfloor ⌊ !^\turned h ᶣ +!- ­ !FE E !bU 𝕌 !BGl 𝛌 !MiD 𝐷 !"{W} Ẅ !c{n} ņ !begin 〖 !ltimes ⋉ !^\turned i ᵎ +!0 ∅ !FF F !bV 𝕍 !BGm 𝛍 !MiE 𝐸 !"{X} Ẍ !c{r} ŗ !below ┬ !mapsto ↦ !^\turned m ᵚ +!@ @ !FG G !bW 𝕎 !BGn 𝛎 !MiF 𝐹 !"{Y} Ÿ !c{s} ş !cdots ⋯ !models ⊧ !^\turned r ʴ +!C ∁ !FH H !bX 𝕏 !BGo 𝛚 !MiG 𝐺 !"{a} ä !c{t} ţ !close ┤ !nVDash ⊯ !^\turned v ᶺ +!H ̋ !FI I !bY 𝕐 !BGp 𝛙 !MiH 𝐻 !"{e} ë !d-u- ⇵ !coint ∲ !nVdash ⊮ !^\turned w ꭩ +!I ⋂ !FJ J !bZ ℤ !BGr 𝛒 !MiI 𝐼 !"{h} ḧ !d\.S Ṩ !colon ₡ !nequiv ≢ !^\turned y 𐞠 +!L Ł !FK K !ba 𝕒 !BGs 𝛔 !MiJ 𝐽 !"{i} ï !d\.s ṩ !dashv ⊣ !nsimeq ≄ !circledast ⊛ +!O Ø !FL L !bb 𝕓 !BGt 𝛕 !MiK 𝐾 !"{o} ö !d\=L Ḹ !dddot ⃛ !numero № !complement ∁ +!P ¶ !FM M !bc 𝕔 !BGu 𝛖 !MiL 𝐿 !"{t} ẗ !d\=R Ṝ !ddots ⋱ !nvDash ⊭ !curlywedge ⋏ +!S § !FN N !bd 𝕕 !BGx 𝛏 !MiM 𝑀 !"{u} ü !d\=l ḹ !delta δ !nvdash ⊬ !eqslantgtr ⋝ +!^ ̂ !FO O !be 𝕖 !BGz 𝛇 !MiN 𝑁 !"{w} ẅ !d\=r ṝ !doteq ≐ !oiiint ∰ !gtreqqless ⋛ +!k ̨ !FP P !bf 𝕗 !BPi 𝚷 !MiO 𝑂 !"{x} ẍ !ddag ‡ !equiv ≡ !ominus ⊖ !lessapprox ≲ +!v ̌ !FQ Q !bg 𝕘 !Box □ !MiP 𝑃 !"{y} ÿ !def= ≝ !frac1 ⅟ !oslash ⊘ !lesseqqgtr ⋚ +!x × !FR R !bh 𝕙 !Bpi 𝛑 !MiQ 𝑄 !'\.S Ṥ !defs ≙ !frown ⌢ !otimes ⊗ !longmapsto ⟼ +!| ∣ !FS S !bi 𝕚 !Cap ⋒ !MiR 𝑅 !'\.s ṥ !dong ₫ !gamma γ !permil ‰ !mathscr{I} ℐ +!!! ‼ !FT T !bj 𝕛 !Chi Χ !MiS 𝑆 !'\AE Ǽ !d{A} Ạ !gimel ℷ !peseta ₧ !nLeftarrow ⇍ +!!? ⁉ !FU U !bk 𝕜 !Cup ⋓ !MiT 𝑇 !'\ae ǽ !d{B} Ḅ !gneqq ≩ !pounds £ !nleftarrow ↚ +!"' “ !FV V !bl 𝕝 !Dei Ϯ !MiU 𝑈 !'{A} Á !d{D} Ḍ !gnsim ⋧ !pprime ″ !nsubseteqq ⊈ +!"< « !FW W !bm 𝕞 !Eta Η !MiV 𝑉 !'{C} Ć !d{E} Ẹ !iiint ∭ !preceq ≼ !nsupseteqq ⊉ +!"> » !FX X !bn 𝕟 !Fei Ϥ !MiW 𝑊 !'{E} É !d{H} Ḥ !imath ı !propto ∝ !precapprox ≾ +!"A Ä !FY Y !bo 𝕠 !GTH Θ !MiX 𝑋 !'{G} Ǵ !d{I} Ị !infty ∞ !rangle ⟩ !prohibited 🛇 +!"E Ë !FZ Z !bp 𝕡 !Gl- ƛ !MiY 𝑌 !'{I} Í !d{K} Ḳ !jmath ȷ !rddots ⋰ !registered ® +!"H Ḧ !Fa a !bq 𝕢 !Glb ⨅ !MiZ 𝑍 !'{K} Ḱ !d{L} Ḷ !kappa κ !rfloor ⌋ !rightarrow → +!"I Ï !Fb b !br 𝕣 !Gth θ !Mia 𝑎 !'{L} Ĺ !d{M} Ṃ !koppa ϟ !rtimes ⋊ !smallamalg ∐ +!"O Ö !Fc c !bs 𝕤 !H{} ˝ !Mib 𝑏 !'{M} Ḿ !d{N} Ṇ !lamda λ !square □ !smallsmile ⌣ +!"U Ü !Fd d !bt 𝕥 !Lsh ↰ !Mic 𝑐 !'{N} Ń !d{O} Ọ !lceil ⌈ !squb=n ⋢ !sqsubseteq ⊑ +!"W Ẅ !Fe e !bv 𝕧 !Lub ⨆ !Mid 𝑑 !'{O} Ó !d{R} Ṛ !ldata 《 !squp=n ⋣ !sqsupseteq ⊒ +!"X Ẍ !Ff f !bw 𝕨 !MCA 𝓐 !Mie 𝑒 !'{P} Ṕ !d{S} Ṣ !ldots … !stigma ϛ !subsetneqq ⊊ +!"Y Ÿ !Fg g !by 𝕪 !MCB 𝓑 !Mif 𝑓 !'{R} Ŕ !d{T} Ṭ !lneqq ≨ !subset ⊂ !succapprox ≿ +!"` „ !Fh h !bz 𝕫 !MCC 𝓒 !Mig 𝑔 !'{S} Ś !d{U} Ụ !lnsim ⋦ !succeq ≽ !supsetneqq ⊋ +!"a ä !Fi i !cC Ç !MCD 𝓓 !Mih ℎ !'{U} Ú !d{V} Ṿ !manat ₼ !supset ⊃ !textlquill ⁅ +!"e ë !Fj j !cD Ḑ !MCE 𝓔 !Mii 𝑖 !'{W} Ẃ !d{W} Ẉ !micro µ !textmu µ !textnumero № +!"h ḧ !Fk k !cE Ȩ !MCF 𝓕 !Mij 𝑗 !'{Y} Ý !d{Y} Ỵ !minus − !tugrik ₮ !textrecipe ℞ +!"i ï !Fl l !cG Ģ !MCG 𝓖 !Mik 𝑘 !'{Z} Ź !d{Z} Ẓ !nabla ∇ !u\'{A} Ắ !textrquill ⁆ +!"o ö !Fm m !cH Ḩ !MCH 𝓗 !Mil 𝑙 !'{a} á !d{a} ạ !naira ₦ !u\'{a} ắ !underbrace ⏟ +!"t ẗ !Fn n !cK Ķ !MCI 𝓘 !Mim 𝑚 !'{c} ć !d{b} ḅ !ncong ≇ !u\`{A} Ằ !underparen ⏝ +!"u ü !Fo o !cL Ļ !MCJ 𝓙 !Min 𝑛 !'{e} é !d{d} ḍ !ngeqq ≱ !u\`{a} ằ !upuparrows ⇈ +!"w ẅ !Fp p !cN Ņ !MCK 𝓚 !Mio 𝑜 !'{g} ǵ !d{e} ẹ !nleqq ≰ !u\d{A} Ặ !varepsilon ε +!"x ẍ !Fq q !cR Ŗ !MCL 𝓛 !Mip 𝑝 !'{i} í !d{h} ḥ !nless ≮ !u\d{a} ặ !Rrightarrow ⇛ +!"y ÿ !Fr r !cS Ş !MCM 𝓜 !Miq 𝑞 !'{k} ḱ !d{i} ị !notin ∉ !u\~{A} Ẵ !Updownarrow ⇕ +!'A Á !Fs s !cT Ţ !MCN 𝓝 !Mir 𝑟 !'{l} ĺ !d{k} ḳ !nprec ⊀ !u\~{a} ẵ !^\capital b 𐞄 +!'C Ć !Ft t !cc ç !MCO 𝓞 !Mis 𝑠 !'{m} ḿ !d{l} ḷ !nsucc ⊁ !v\.{S} Ṧ !^\capital g 𐞒 +!'E É !Fu u !cd ḑ !MCP 𝓟 !Mit 𝑡 !'{n} ń !d{m} ṃ !oiint ∯ !v\.{s} ṧ !^\capital h 𐞖 +!'G Ǵ !Fv v !ce ȩ !MCQ 𝓠 !Miu 𝑢 !'{o} ó !d{n} ṇ !omega ω !varkai ϗ !^\capital i ᶦ +!'I Í !Fw w !cg ģ !MCR 𝓡 !Miv 𝑣 !'{p} ṕ !d{o} ọ !oplus ⊕ !varphi φ !^\capital l ᶫ +!'K Ḱ !Fx x !ch ḩ !MCS 𝓢 !Miw 𝑤 !'{r} ŕ !d{r} ṛ !ounce ℥ !varrho ϱ !^\capital n ᶰ +!'L Ĺ !Fy y !ck ķ !MCT 𝓣 !Mix 𝑥 !'{s} ś !d{s} ṣ !pound £ !veebar ⊻ !^\capital r 𐞪 +!'M Ḿ !Fz z !cn ņ !MCU 𝓤 !Miy 𝑦 !'{u} ú !d{t} ṭ !prcue ≼ !~\"{O} Ṏ !^\capital u ᶸ +!'N Ń !GA Α !cr ŗ !MCV 𝓥 !Miz 𝑧 !'{w} ẃ !d{u} ụ !prime ′ !~\"{o} ṏ !^\capital y 𐞲 +!'O Ó !GB Β !cs ş !MCW 𝓦 !Psi Ψ !'{y} ý !d{v} ṿ !ratio ∶ !~\'{O} Ṍ !^\greek phi ᵠ +!'P Ṕ !GC Χ !ct ţ !MCX 𝓧 !Rho Ρ !'{z} ź !d{w} ẉ !rceil ⌉ !~\'{U} Ṹ !^\rams horn 𐞑 +!'R Ŕ !GD Δ !d- ↓ !MCY 𝓨 !Rsh ↱ !.\=A Ǡ !d{y} ỵ !rdata 》 !~\'{o} ṍ !^\turned ae ᵆ +!'S Ś !GE Ε !d= ⇓ !MCZ 𝓩 !San Ϻ !.\=O Ȱ !d{z} ẓ !rrect ▢ !~\'{u} ṹ !backepsilon ∍ +!'U Ú !GF Φ !dd ⅆ !MCa 𝓪 !Sho Ϸ !.\=a ǡ !euro € !ruble ₽ !~\={O} Ȭ !blacksmiley ☻ +!'W Ẃ !GG Γ !dh ð !MCb 𝓫 !Tau Τ !.\=o ȱ !flat ♭ !rupee ₨ !~\={o} ȭ !blacksquare ▪ +!'Y Ý !GH Η !dz ↯ !MCc 𝓬 !Yot Ϳ !.{A} Ȧ !flqq « !sampi ϡ !Diamond ◇ !circledcirc ⊚ +!'Z Ź !GI Ι !ee ⅇ !MCd 𝓭 !^Gb ᵝ !.{B} Ḃ !frqq » !sharp ♯ !Digamma Ϝ !circleddash ⊝ +!'a á !GK Κ !em — !MCe 𝓮 !^Gc ᵡ !.{C} Ċ !geqq ≧ !shima ϭ !Epsilon Ε !curlyeqprec ⋞ +!'c ć !GL Λ !en – !MCf 𝓯 !^Gd ᵟ !.{D} Ḋ !gets ← !sigma σ !Omicron Ο !curlyeqsucc ⋟ +!'e é !GM Μ !ex ∃ !MCg 𝓰 !^Ge ᵋ !.{E} Ė !glqq „ !simeq ≃ !Uparrow ⇑ !curlypreceq ≼ +!'g ǵ !GN Ν !ge ≥ !MCh 𝓱 !^Gf ᵠ !.{F} Ḟ !gneq ≩ !smash ⬍ !Upsilon Υ !diamondsuit ♢ +!'i í !GO Ω !gg ≫ !MCi 𝓲 !^Gg ᵞ !.{G} Ġ !grqq “ !smile ⌣ !^\alpha ᵅ !eqslantless ⋜ +!'k ḱ !GP Ψ !ii ⅈ !MCj 𝓳 !^a_ ª !.{H} Ḣ !hbar ℏ !sqcap ⊓ !^\delta ᵟ !nRightarrow ⇏ +!'l ĺ !GR Ρ !in ∈ !MCk 𝓴 !^l- ⃖ !.{I} İ !heta ͱ !sqcup ⊔ !^\gamma ˠ !nrightarrow ↛ +!'m ḿ !GS Σ !jj ⅉ !MCl 𝓵 !^lr ⃡ !.{M} Ṁ !hori ϩ !squb= ⊑ !^\schwa ᵊ !ordfeminine ª +!'n ń !GT Τ !kA Ą !MCm 𝓶 !^o_ º !.{N} Ṅ !iint ∬ !squp= ⊒ !^\theta ᶿ !partnership ㉐ +!'o ó !GU Υ !kE Ę !MCn 𝓷 !^r- ⃗ !.{O} Ȯ !iota ι !sub=n ⊈ !^\u bar ᶶ !precnapprox ⋨ +!'p ṕ !GX Ξ !kI Į !MCo 𝓸 !_Gb ᵦ !.{P} Ṗ !k\=O Ǭ !sup=n ⊉ !afghani ؋ !radioactive ☢ +!'r ŕ !GZ Ζ !kO Ǫ !MCp 𝓹 !_Gc ᵪ !.{R} Ṙ !k\=o ǭ !surd3 ∛ !austral ₳ !straightphi φ +!'s ś !Ga α !kU Ų !MCq 𝓺 !_Gf ᵩ !.{S} Ṡ !khei ϧ !surd4 ∜ !backsim ∽ !succcurlyeq ≽ +!'u ú !Gb β !ka ą !MCr 𝓻 !_Gg ᵧ !.{T} Ṫ !k{A} Ą !tenge ₸ !because ∵ !succnapprox ⋩ +!'w ẃ !Gc χ !ke ę !MCs 𝓼 !_Gr ᵨ !.{W} Ẇ !k{E} Ę !theta θ !between ≬ !thickapprox ≈ +!'y ý !Gd δ !ki į !MCt 𝓽 !_lr ͍ !.{X} Ẋ !k{I} Į !times × !bigcirc ◯ !uncertainty ⯑ +!'z ź !Ge ε !ko ǫ !MCu 𝓾 !all ∀ !.{Y} Ẏ !k{O} Ǫ !uplus ⊎ !bigodot ⨀ !updownarrow ↕ +!(= ≘ !Gf φ !ku ų !MCv 𝓿 !and ∧ !.{Z} Ż !k{U} Ų !u{\i} ĭ !bigstar ★ !^\Reversed E ᴲ +!(b ⟅ !Gg γ !l- ← !MCw 𝔀 !ast ∗ !.{a} ȧ !k{a} ą !vDash ⊨ !bitcoin ₿ !^\Reversed N ᴻ +!(| ⦇ !Gh η !l= ⇐ !MCx 𝔁 !bGG ℾ !.{b} ḃ !k{e} ę !varpi ϖ !boxplus ⊞ !^\capital aa 𐞀 +!)b ⟆ !Gi ι !le ≤ !MCy 𝔂 !bGP ℿ !.{c} ċ !k{i} į !vdash ⊢ !caution ☡ !^\capital oe 𐞣 +!*= ≛ !Gk κ !ll ≪ !MCz 𝔃 !bGS ⅀ !.{d} ḋ !k{o} ǫ !vdots ⋮ !celsius ℃ !^\dz digraph 𐞇 +!+ ⊹ !Gl λ !lq ‘ !MIA 𝑨 !bGg ℽ !.{e} ė !k{u} ų !v{\i} ǐ !coloneq ≔ !^\ls digraph 𐞙 +!-: ∹ !Gm μ !m= ≞ !MIB 𝑩 !bGp ℼ !.{f} ḟ !l-r- ⇆ !v{\j} ǰ !ddagger ‡ !^\lz digraph 𐞚 +!-> → !Gn ν !mp ∓ !MIC 𝑪 !bot ⊥ !.{g} ġ !lari ₾ !wedge ∧ !diamond ⋄ !^\reversed e 𐞎 +!-o ⊸ !Go ω !mu μ !MID 𝑫 !bra ⟨ !.{h} ḣ !lbag ⟅ !~{\i} ĩ !digamma ϝ !^\sideways u ᵙ +!-| ⊣ !Gp ψ !ne ≠ !MIE 𝑬 !bub • !.{m} ṁ !ldiv ∕ !"\'{I} Ḯ !dotplus ∔ !^\top half o ᵔ +!-~ ≂ !Gr ρ !ni ∋ !MIF 𝑭 !but ‣ !.{n} ṅ !leqq ≦ !"\'{U} Ǘ !drachma ₯ !^\ts digraph 𐞬 +!.+ ∔ !Gs σ !nu ν !MIG 𝑮 !buw ◦ !.{o} ȯ !lira ₤ !"\'{i} ḯ !epsilon ϵ !blacklozenge ✦ +!.- ∸ !Gt τ !o* ⊛ !MIH 𝑯 !cap ∩ !.{p} ṗ !lneq ≨ !"\'{u} ǘ !eqarray █ !construction 🚧 +!.= ≐ !Gu υ !o+ ⊕ !MII 𝑰 !chi χ !.{r} ṙ !lnot ¬ !"\={A} Ǟ !eqcolon ≕ !ordmasculine º +!.A Ȧ !Gx ξ !o- ⊝ !MIJ 𝑱 !ci. ◎ !.{s} ṡ !lr-- ⟷ !"\={O} Ȫ !gtrless ≷ !risingdotseq ≓ +!.B Ḃ !Gz ζ !o. ⊙ !MIK 𝑲 !ciO ◯ !.{t} ṫ !lr-n ↮ !"\={U} Ǖ !guarani ₲ !textcircledP ℗ +!.C Ċ !HO Ő !o/ ⊘ !MIL 𝑳 !cib ● !.{w} ẇ !lr=n ⇎ !"\={a} ǟ !hryvnia ₴ !textdiscount ⁒ +!.D Ḋ !HU Ű !o= ⊜ !MIM 𝑴 !ciw ○ !.{x} ẋ !male ♂ !"\={o} ȫ !leadsto ↝ !triangledown ▿ +!.E Ė !Ho ő !oe œ !MIN 𝑵 !clL ⌊ !.{y} ẏ !mill ₥ !"\={u} ǖ !lessdot ⋖ !triangleleft ◃ +!.F Ḟ !Hu ű !oo ⊚ !MIO 𝑶 !clR ⌋ !.{z} ż !ngeq ≱ !"\`{U} Ǜ !lessgtr ≶ !Longleftarrow ⇐ +!.G Ġ !Im ℑ !or ∨ !MIP 𝑷 !cll ⌞ !<--> ⟷ !ngtr ≯ !"\`{u} ǜ !lesssim ≲ !^\b with hook 𐞅 +!.H Ḣ !Ll ⋘ !ox ⊗ !MIQ 𝑸 !clr ⌟ !<->n ↮ !nleq ≰ !"\v{U} Ǚ !lozenge ✧ !^\c with curl ᶝ +!.I İ !Mu Μ !pi π !MIR 𝑹 !cuL ⌈ !<=>n ⇎ !nmid ∤ !"\v{u} ǚ !maltese ✠ !^\d with hook 𐞌 +!.M Ṁ !Nu Ν !pm ± !MIS 𝑺 !cuR ⌉ !<~nn ≴ !nsim ≁ !'\.{S} Ṥ !napprox ≉ !^\d with tail 𐞋 +!.N Ṅ !O* ⍟ !r- → !MIT 𝑻 !cul ⌜ !=\"U Ṻ !odot ⊙ !'\.{s} ṥ !natural ♮ !^\g with hook 𐞓 +!.O Ȯ !O+ ⨁ !r= ⇒ !MIU 𝑼 !cup ∪ !=\"u ṻ !oint ∮ !'{\AE} Ǽ !nearrow ↗ !^\greek gamma ᵞ +!.P Ṗ !O. ⨀ !rq ’ !MIV 𝑽 !cur ⌝ !=\'E Ḗ !perp ⊥ !'{\ae} ǽ !newline 
 !^\h with hook ʱ +!.R Ṙ !OE Œ !ss ß !MIW 𝑾 !c{} ¸ !=\'O Ṓ !peso ₱ !.\={A} Ǡ !nexists ∄ !^\l with belt 𐞛 +!.S Ṡ !Or ⋁ !t= ≜ !MIX 𝑿 !d-2 ⇊ !=\'e ḗ !prec ≺ !.\={O} Ȱ !nomisma 𐆎 !^\ligature oe ꟹ +!.T Ṫ !Ox ⨂ !th þ !MIY 𝒀 !d-| ↧ !=\'o ṓ !prod ∏ !.\={a} ǡ !npreceq ⋠ !^\m with hook ᶬ +!.W Ẇ !Pi Π !to → !MIZ 𝒁 !d== ⟱ !=\AE Ǣ !qdrt ∜ !.\={o} ȱ !nsubset ⊄ !^\r with tail 𐞨 +!.X Ẋ !Re ℜ !u+ ⊎ !MIa 𝒂 !dag † !=\`E Ḕ !quad   !=\"{U} Ṻ !nsucceq ⋡ !^\s with curl 𐞺 +!.Y Ẏ !TH Þ !u- ↑ !MIb 𝒃 !dd- ↡ !=\`O Ṑ !r-l- ⇄ !=\"{u} ṻ !nsupset ⊅ !^\s with hook ᶳ +!.Z Ż !U+ ⨄ !u. ⊍ !MIc 𝒄 !dei ϯ !=\`e ḕ !rbag ⟆ !=\'{E} Ḗ !nwarrow ↖ !^\v with hook ᶹ +!.a ȧ !U. ⨃ !u= ⇑ !MId 𝒅 !di. ◈ !=\`o ṑ !rect ▭ !=\'{O} Ṓ !omicron ο !^\z with curl ᶽ +!.b ḃ !Un ⋃ !uA Ă !MIe 𝒆 !dib ◆ !=\ae ǣ !rial ﷼ !=\'{e} ḗ !partial ∂ !bigtriangleup △ +!.c ċ !Uo ő !uE Ĕ !MIf 𝒇 !div ÷ !={A} Ā !sdiv ⁄ !=\'{o} ṓ !phantom ⟡ !blacktriangle ▴ +!.d ḋ !Xi Ξ !uG Ğ !MIg 𝒈 !diw ◇ !={E} Ē !shei ϣ !=\`{E} Ḕ !pilcrow ¶ !divideontimes ⋇ +!.e ė ![[ ⟦ !uI Ĭ !MIh 𝒉 !dl- ↙ !={G} Ḡ !sqrt √ !=\`{O} Ṑ !ppprime ‴ !fallingdotseq ≒ +!.f ḟ !]] ⟧ !uO Ŏ !MIi 𝒊 !dl= ⇙ !={I} Ī !squb ⊏ !=\`{e} ḕ !precsim ≾ !hookleftarrow ↩ +!.g ġ !^( ⁽ !uU Ŭ !MIj 𝒋 !dr- ↘ !={O} Ō !squp ⊐ !=\`{o} ṑ !searrow ↘ !leftarrowtail ↢ +!.h ḣ !^) ⁾ !ua ă !MIk 𝒌 !dr= ⇘ !={U} Ū !st12 ✹ !={\AE} Ǣ !section § !leftharpoonup ↼ +!.m ṁ !^+ ⁺ !ue ĕ !MIl 𝒍 !ell ℓ !={Y} Ȳ !star ⋆ !={\ae} ǣ !sqrt[3] ∛ !longleftarrow ⟵ +!.n ṅ !^- ⁻ !ug ğ !MIm 𝒎 !end 〗 !={a} ā !sub= ⊆ !Bbb{1} 𝟙 !sqrt[4] ∜ !looparrowleft ↫ +!.o ȯ !^0 ⁰ !ui ĭ !MIn 𝒏 !eta η !={e} ē !subn ⊄ !Bbb{2} 𝟚 !succsim ≿ !measuredangle ∡ +!.p ṗ !^1 ¹ !un ∪ !MIo 𝒐 !exn ∄ !={g} ḡ !succ ≻ !Bbb{A} 𝔸 !swarrow ↙ !ntriangleleft ⋪ +!.r ṙ !^2 ² !uo ŏ !MIp 𝒑 !fei ϥ !={i} ī !sup= ⊇ !Bbb{B} 𝔹 !textwon ₩ !shortparallel ∥ +!.s ṡ !^3 ³ !uu ŭ !MIq 𝒒 !flq ‹ !={o} ō !supn ⊅ !Bbb{C} ℂ !uparrow ↑ !smallsetminus ∖ +!.t ṫ !^4 ⁴ !vA Ǎ !MIr 𝒓 !frq › !={u} ū !surd √ !Bbb{D} 𝔻 !upsilon υ !textbigcircle ⃝ +!.w ẇ !^5 ⁵ !vC Č !MIs 𝒔 !gen ≱ !={y} ȳ !u-d- ⇅ !Bbb{E} 𝔼 !varbeta ϐ !textestimated ℮ +!.x ẋ !^6 ⁶ !vD Ď !MIt 𝒕 !ggg ⋙ !>~nn ≵ !u\'A Ắ !Bbb{F} 𝔽 !warning ⚠ !triangleright ▹ +!.y ẏ !^7 ⁷ !vE Ě !MIu 𝒖 !glb ⊓ !BGTH 𝚯 !u\'a ắ !Bbb{G} 𝔾 !BOmicron 𝚶 !upleftharpoon ↿ +!.z ż !^8 ⁸ !vG Ǧ !MIv 𝒗 !glq ‚ !BGth 𝛉 !u\`A Ằ !Bbb{H} ℍ !Bomicron 𝛐 !Leftrightarrow ⇔ +!:: ∷ !^9 ⁹ !vH Ȟ !MIw 𝒘 !grq ‘ !Beta Β !u\`a ằ !Bbb{I} 𝕀 !^\h hook ʱ !Longrightarrow ⇒ +!:= ≔ !^= ⁼ !vI Ǐ !MIx 𝒙 !iff ⇔ !Heta Ͱ !u\~A Ẵ !Bbb{J} 𝕁 !^\open e ᵋ !^\closed omega 𐞤 +!:~ ∻ !^B ᴮ !vK Ǩ !MIy 𝒚 !inf ∞ !Hori Ϩ !u\~a ẵ !Bbb{K} 𝕂 !^\open o ᵓ !^\dezh digraph 𐞊 +!<- ← !^D ᴰ !vL Ľ !MIz 𝒛 !inn ∉ !H{O} Ő !ud-| ↨ !Bbb{L} 𝕃 !angstrom Å !^\feng digraph 𐞐 +!<< ⟪ !^F ꟳ !vN Ň !McA 𝒜 !int ∫ !H{U} Ű !u{A} Ă !Bbb{M} 𝕄 !approxeq ≊ !^\tesh digraph 𐞮 +! ↢ !Khei Ϧ !u{U} Ŭ !Bbb{R} ℝ !biguplus ⨄ !leftleftarrows ⇇ +!=> ⇒ !^Q ꟴ !vZ Ž !McG 𝒢 !l-n ↚ !Shei Ϣ !u{a} ă !Bbb{S} 𝕊 !bigwedge ⋀ !leftrightarrow ↔ +!=A Ā !^R ᴿ !va ǎ !McH ℋ !l-| ↤ !U{o} ő !u{e} ĕ !Bbb{T} 𝕋 !boxminus ⊟ !leftthreetimes ⋋ +!=E Ē !^S Ŝ !vc č !McI ℐ !l== ⇚ !Vert ‖ !u{g} ğ !Bbb{U} 𝕌 !boxtimes ⊠ !longrightarrow ⟶ +!=G Ḡ !^T ᵀ !vd ď !McJ 𝒥 !l=n ⇍ !Zeta Ζ !u{i} ĭ !Bbb{V} 𝕍 !circledR ® !looparrowright ↬ +!=I Ī !^V ⱽ !ve ě !McK 𝒦 !ldq “ !^Gth ᶿ !u{o} ŏ !Bbb{W} 𝕎 !circledS Ⓢ !multiplication × +!=O Ō !^Y Ŷ !vg ǧ !McL ℒ !len ≰ !^\'A Ấ !u{u} ŭ !Bbb{X} 𝕏 !clubsuit ♣ !nshortparallel ∦ +!=U Ū !^Z Ẑ !vh ȟ !McM ℳ !lhd ◁ !^\'E Ế !v\.S Ṧ !Bbb{Y} 𝕐 !cruzeiro ₢ !ntriangleright ⋫ +!=Y Ȳ !^b ᵇ !vi ǐ !McN 𝒩 !ll- ↞ !^\'O Ố !v\.s ṧ !Bbb{Z} ℤ !curlyvee ⋎ !rightarrowtail ↣ +!=a ā !^d ᵈ !vj ǰ !McO 𝒪 !lr- ↔ !^\'a ấ !vbar │ !Bumpeq ≎ !currency ¤ !rightharpoonup ⇀ +!=e ē !^f ᶠ !vk ǩ !McP 𝒫 !lr= ⇔ !^\'e ế !v{A} Ǎ !Frowny ☹ !diameter ⌀ !sphericalangle ∢ +!=g ḡ !^k ᵏ !vl ľ !McQ 𝒬 !lr~ ↭ !^\'o ố !v{C} Č !Gangia Ϫ !division ÷ !textopenbullet ◦ +!=i ī !^m ᵐ !vn ň !McR ℛ !lub ⊔ !^\Ae ᴭ !v{D} Ď !Lambda Λ !doteqdot ≑ !trianglelefteq ⊴ +!=n ≠ !^n ⁿ !vo ǒ !McS 𝒮 !mho ℧ !^\Ou ᴽ !v{E} Ě !Letter ✉ !emptyset ∅ !uprightharpoon ↾ +!=u ū !^p ᵖ !vr ř !McT 𝒯 !mid ∣ !^\`A Ầ !v{G} Ǧ !Smiley ☺ !geqslant ≥ !^\H With Stroke ꟸ +!=y ȳ !^q 𐞥 !vs š !McU 𝒰 !neg ¬ !^\`E Ề !v{H} Ȟ !Stigma Ϛ !gnapprox ⋧ !^\bottom half o ᵕ +!>= ≥ !^t ᵗ !vt ť !McV 𝒱 !neq ≠ !^\`O Ồ !v{I} Ǐ !Subset ⋐ !hphantom ⬄ !^\h with stroke 𐞕 +!>> ⟫ !^x ˣ !vu ǔ !McW 𝒲 !nin ∌ !^\`a ầ !v{K} Ǩ !Supset ⋑ !intercal ⊺ !^\i with stroke ᶤ +!>n ≯ !_( ₍ !vz ž !McX 𝒳 !not ̸ !^\`e ề !v{L} Ľ !Vvdash ⊪ !leqslant ≤ !^\o with stroke 𐞢 +!>~ ≳ !_) ₎ !wp ℘ !McY 𝒴 !o-- ⊖ !^\`o ồ !v{N} Ň !^\'{A} Ấ !llcorner ⌞ !^\turned open e ᵌ +!?= ≟ !_+ ₊ !wr ≀ !McZ 𝒵 !ohm Ω !^\ae 𐞃 !v{O} Ǒ !^\'{E} Ế !lnapprox ⋦ !^\turned r hook ʵ +!?? ⁇ !_- ₋ !xi ξ !Mca 𝒶 !or= ≚ !^\~A Ẫ !v{R} Ř !^\'{O} Ố !lrcorner ⌟ !bigtriangledown ▽ +!AA Å !_0 ₀ !z: ⦂ !Mcb 𝒷 !pab ▰ !^\~E Ễ !v{S} Š !^\'{a} ấ !multimap ⊸ !circlearrowleft ↺ +!AE Æ !_1 ₁ !{{ ⦃ !Mcc 𝒸 !paw ▱ !^\~O Ỗ !v{T} Ť !^\'{e} ế !parallel ∥ !curvearrowright ↷ +!B0 𝟎 !_2 ₂ !|) ⦈ !Mcd 𝒹 !per ⅌ !^\~a ẫ !v{U} Ǔ !^\'{o} ố !pppprime ⁗ !downleftharpoon ⇃ +!B1 𝟏 !_3 ₃ !|- ⊢ !Mce ℯ !phi ϕ !^\~e ễ !v{Z} Ž !^\`{A} Ầ !precnsim ⋨ !leftharpoondown ↽ +!B2 𝟐 !_4 ₄ !|= ⊨ !Mcf 𝒻 !psi ψ !^\~o ỗ !v{a} ǎ !^\`{E} Ề !setminus ∖ !leftrightarrows ⇆ +!B3 𝟑 !_5 ₅ !|> ⦊ !Mcg ℊ !qed ∎ !^{A}  !v{c} č !^\`{O} Ồ !shortmid ∣ !nLeftrightarrow ⇎ +!B4 𝟒 !_6 ₆ !|n ∤ !Mch 𝒽 !r-- ⟶ !^{C} Ĉ !v{d} ď !^\`{a} ầ !spesmilo ₷ !nleftrightarrow ↮ +!B5 𝟓 !_7 ₇ !|| ∥ !Mci 𝒾 !r-2 ⇉ !^{E} Ê !v{e} ě !^\`{e} ề !sqsubset ⊏ !ntrianglelefteq ⋬ +!B6 𝟔 !_8 ₈ !}} ⦄ !Mcj 𝒿 !r-3 ⇶ !^{G} Ĝ !v{g} ǧ !^\`{o} ồ !sqsupset ⊐ !rightleftarrows ⇄ +!B7 𝟕 !_9 ₉ !~- ≃ !Mck 𝓀 !r-> ↣ !^{H} Ĥ !v{h} ȟ !^\beta ᵝ !subseteq ⊆ !rightthreetimes ⋌ +!B8 𝟖 !_= ₌ !~= ≅ !Mcl 𝓁 !r-n ↛ !^{I} Î !v{i} ǐ !^\d{A} Ậ !succnsim ⋩ !squigarrowright ⇝ +!B9 𝟗 !__ _ !~A à !Mcm 𝓂 !r-o ⊸ !^{J} Ĵ !v{j} ǰ !^\d{E} Ệ !supseteq ⊇ !textinterrobang ‽ +!BA 𝐀 !_a ₐ !~E Ẽ !Mcn 𝓃 !r-| ↦ !^{O} Ô !v{k} ǩ !^\d{O} Ộ !textbaht ฿ !textmusicalnote ♪ +!BB 𝐁 !_e ₑ !~I Ĩ !Mco ℴ !r== ⇛ !^{S} Ŝ !v{l} ľ !^\d{a} ậ !textdied ✝ !trianglerighteq ⊵ +!BC 𝐂 !_h ₕ !~N Ñ !Mcp 𝓅 !r=n ⇏ !^{U} Û !v{n} ň !^\d{e} ệ !textlira ₤ !vartriangleleft ⊲ +!BD 𝐃 !_i ᵢ !~O Õ !Mcq 𝓆 !rdq ” !^{W} Ŵ !v{o} ǒ !^\d{o} ộ !textpeso ₱ !^\heng with hook 𐞗 +!BE 𝐄 !_j ⱼ !~U Ũ !Mcr 𝓇 !rhd ▷ !^{Y} Ŷ !v{r} ř !^\heng ꭜ !thicksim ∼ !circlearrowright ↻ +!BF 𝐅 !_k ₖ !~V Ṽ !Mcs 𝓈 !rho ρ !^{Z} Ẑ !v{s} š !^\iota ᶥ !triangle ▵ !downrightharpoon ⇂ +!BG 𝐆 !_l ₗ !~Y Ỹ !Mct 𝓉 !rr- ↠ !^{a} â !v{t} ť !^\lezh 𐞞 !ulcorner ⌜ !ntrianglerighteq ⋭ +!BH 𝐇 !_m ₘ !~a ã !Mcu 𝓊 !san ϻ !^{c} ĉ !v{u} ǔ !^\~{A} Ẫ !underbar ▁ !rightharpoondown ⇁ +!BI 𝐈 !_n ₙ !~e ẽ !Mcv 𝓋 !sbs ﹨ !^{e} ê !v{z} ž !^\~{E} Ễ !undertie ‿ !rightrightarrows ⇉ +!BJ 𝐉 !_o ₒ !~i ĩ !Mcw 𝓌 !sho ϸ !^{g} ĝ !zeta ζ !^\~{O} Ỗ !urcorner ⌝ !twoheadleftarrow ↞ +!BK 𝐊 !_p ₚ !~o õ !Mcx 𝓍 !sim ∼ !^{h} ĥ !||-n ⊮ !^\~{a} ẫ !varkappa ϰ !vartriangleright ⊳ +!BL 𝐋 !_r ᵣ !~u ũ !Mcy 𝓎 !som ⃀ !^{i} î !||=n ⊯ !^\~{e} ễ !varprime ′ !^\r with fishhook 𐞩 +!BM 𝐌 !_s ₛ !~v ṽ !Mcz 𝓏 !sq. ▣ !^{j} ĵ !|||- ⊪ !^\~{o} ỗ !varsigma ς !^\reversed open e ᶟ +!BN 𝐍 !_t ₜ !~y ỹ !MfA 𝔄 !sqo ▢ !^{o} ô !~\"O Ṏ !^{TEL} ℡ !vartheta ϑ !blacktriangledown ▾ +!BO 𝐎 !_u ᵤ !~~ ≈ !MfB 𝔅 !st6 ✶ !^{s} ŝ !~\"o ṏ !approx ≈ !vphantom ⇳ !blacktriangleleft ◂ +!BP 𝐏 !_x ₓ !"{} ¨ !MfC ℭ !st8 ✴ !^{u} û !~\'O Ṍ !asmash ⬆ !Downarrow ⇓ !leftrightharpoons ⇋ +!BQ 𝐐 !_~ ̰ !'\O Ǿ !MfD 𝔇 !sub ⊂ !^{w} ŵ !~\'U Ṹ !bigcap ⋂ !Leftarrow ⇐ !rightleftharpoons ⇌ +!BR 𝐑 !`A À !'\o ǿ !MfE 𝔈 !sum ∑ !^{y} ŷ !~\'o ṍ !bigcup ⋃ !^\upsilon ᶷ !textcolonmonetary ₡ +!BS 𝐒 !`E È !'{} ´ !MfF 𝔉 !sup ⊃ !^{z} ẑ !~\'u ṹ !bigvee ⋁ !backprime ‵ !textreferencemark ※ +!BT 𝐓 !`I Ì !--> ⟶ !MfG 𝔊 !tau τ !`{A} À !~\=O Ȭ !bowtie ⋈ !backsimeq ⋍ !twoheadrightarrow ↠ +!BU 𝐔 !`N Ǹ !->n ↛ !MfH ℌ !tie ⁀ !`{E} È !~\=o ȭ !bumpeq ≏ !bigotimes ⨂ !Longleftrightarrow ⇔ +!BV 𝐕 !`O Ò !.=. ≑ !MfI ℑ !top ⊤ !`{I} Ì !~{A} à !c\'{C} Ḉ !biohazard ☣ !^\n with left hook ᶮ +!BW 𝐖 !`U Ù !.{} ˙ !MfJ 𝔍 !u-2 ⇈ !`{N} Ǹ !~{E} Ẽ !c\'{c} ḉ !brokenbar ¦ !^\u with left hook ꭟ +!BX 𝐗 !`W Ẁ !::- ∺ !MfK 𝔎 !u-| ↥ !`{O} Ò !~{I} Ĩ !c\u{E} Ḝ !centerdot · !blacktriangleright ▸ +!BY 𝐘 !`Y Ỳ !<-- ⟵ !MfL 𝔏 !u== ⟰ !`{U} Ù !~{N} Ñ !c\u{e} ḝ !checkmark ✓ !longleftrightarrow ⟷ +!BZ 𝐙 !`a à !<-> ↔ !MfM 𝔐 !ud- ↕ !`{W} Ẁ !~{O} Õ !circeq ≗ !copyright © !textpertenthousand ‱ +!Ba 𝐚 !`e è !<-n ↚ !MfN 𝔑 !ud= ⇕ !`{Y} Ỳ !~{U} Ũ !coprod ∐ !dotsquare ⊡ !^\v with right hook 𐞰 +!Bb 𝐛 !`i ì !<=> ⇔ !MfO 𝔒 !ul- ↖ !`{a} à !~{V} Ṽ !d\.{S} Ṩ !downarrow ↓ !leftrightsquigarrow ↭ +!Bc 𝐜 !`n ǹ !<=n ≰ !MfP 𝔓 !ul= ⇖ !`{e} è !~{Y} Ỹ !d\.{s} ṩ !facsimile ℻ !textfractionsolidus ⁄ +!Bd 𝐝 !`o ò !<~n ⋦ !MfQ 𝔔 !ur- ↗ !`{i} ì !~{a} ã !d\={L} Ḹ !gtrapprox ≳ !^\capital inverted r ʶ +!Be 𝐞 !`u ù !=== ≣ !MfR ℜ !ur= ⇗ !`{n} ǹ !~{e} ẽ !d\={R} Ṝ !gtreqless ⋛ !^\turned r with hook ʵ +!Bf 𝐟 !`w ẁ !==n ≢ !MfS 𝔖 !uu- ↟ !`{o} ò !~{i} ĩ !d\={l} ḹ !gvertneqq ≩ !^\turned y with belt 𐞡 +!Bg 𝐠 !`y ỳ !=>n ⇏ !MfT 𝔗 !u{} ˘ !`{u} ù !~{n} ñ !d\={r} ṝ !heartsuit ♥ !^\capital g with hook 𐞔 +!Bh 𝐡 !aa å !={} ¯ !MfU 𝔘 !vee ∨ !`{w} ẁ !~{o} õ !dagger † !increment ∆ !^\capital l with belt 𐞜 +!Bi 𝐢 !ae æ !>=n ≱ !MfV 𝔙 !v{} ˇ !`{y} ỳ !~{u} ũ !daleth ℸ !lambdabar ƛ !^\j with crossed-tail ᶨ +!Bj 𝐣 !b+ ⊞ !>~n ⋧ !MfW 𝔚 !won ₩ !and= ≙ !~{v} ṽ !ddddot ⃜ !leftarrow ← !^\l with middle tilde ꭞ +!Bk 𝐤 !b- ⊟ !And ⋀ !MfX 𝔛 !yen ¥ !atop ¦ !~{y} ỹ !degree ° !lesseqgtr ⋚ !^\l with palatal hook ᶪ +!Bl 𝐥 !b. ⊡ !BGA 𝚨 !MfY 𝔜 !|-n ⊬ !beta β !'{\O} Ǿ !dsmash ⬇ !llbracket 〚 !^\t with palatal hook ᶵ +!Bm 𝐦 !b0 𝟘 !BGB 𝚩 !MfZ ℨ !|=n ⊭ !beth ℶ !'{\o} ǿ !eqcirc ≖ !lvertneqq ≨ !^\d with hook and tail 𐞍 +!Bn 𝐧 !b1 𝟙 !BGC 𝚾 !Mfa 𝔞 !||- ⊩ !c\'C Ḉ !={\i} ī !exists ∃ !ngeqslant ≱ !^\dz digraph with curl 𐞉 +!Bo 𝐨 !b2 𝟚 !BGD 𝚫 !Mfb 𝔟 !||= ⊫ !c\'c ḉ !Alpha Α !female ♀ !nleqslant ≰ !^\tc digraph with curl 𐞫 +!Bp 𝐩 !b3 𝟛 !BGE 𝚬 !Mfc 𝔠 !||n ∦ !c\uE Ḝ !Delta Δ !forall ∀ !nparallel ∦ !^\capital i with stroke ᶧ +!Bq 𝐪 !b4 𝟜 !BGF 𝚽 !Mfd 𝔡 !~-n ≄ !c\ue ḝ !Gamma Γ !frac12 ½ !nshortmid ∤ !^\dotless j with stroke ᶡ +!Br 𝐫 !b5 𝟝 !BGG 𝚪 !Mfe 𝔢 !~=n ≇ !cdot · !Kappa Κ !frac13 ⅓ !nsubseteq ⊈ !^\l with retroflex hook ᶩ +!Bs 𝐬 !b6 𝟞 !BGH 𝚮 !Mff 𝔣 !~{} ˜ !cedi ₵ !Koppa Ϟ !frac14 ¼ !nsupseteq ⊉ !^\n with retroflex hook ᶯ +!Bt 𝐭 !b7 𝟟 !BGI 𝚰 !Mfg 𝔤 !~~- ≊ !cent ¢ !Lamda Λ !frac15 ⅕ !octagonal 🛑 !^\reversed glottal stop ˤ +!Bu 𝐮 !b8 𝟠 !BGK 𝚱 !Mfh 𝔥 !~~n ≉ !ci.. ◌ !Omega Ω !frac16 ⅙ !overbrace ⏞ !^\t with retroflex hook 𐞯 +!Bv 𝐯 !b9 𝟡 !BGL 𝚲 !Mfi 𝔦 !~~~ ≋ !circ ∘ !Sampi Ϡ !frac18 ⅛ !overparen ⏜ !^\z with retroflex hook ᶼ +!Bw 𝐰 !bA 𝔸 !BGM 𝚳 !Mfj 𝔧 !"\'I Ḯ !comp ∘ !Shima Ϭ !frac23 ⅔ !paragraph ¶ !^\closed reversed open e 𐞏 +!Bx 𝐱 !bB 𝔹 !BGN 𝚴 !Mfk 𝔨 !"\'U Ǘ !cong ≅ !Sigma Σ !frac25 ⅖ !pitchfork ⋔ !^\l with inverted lazy s ꭝ +!By 𝐲 !bC ℂ !BGO 𝛀 !Mfl 𝔩 !"\'i ḯ !c{C} Ç !Theta Θ !frac34 ¾ !rrbracket 〛 !^\turned m with long leg ᶭ +!Bz 𝐳 !bD 𝔻 !BGP 𝚿 !Mfm 𝔪 !"\'u ǘ !c{D} Ḑ !Vdash ⊩ !frac35 ⅗ !spadesuit ♠ !^\turned r with long leg 𐞦 +!DH Ð !bE 𝔼 !BGR 𝚸 !Mfn 𝔫 !"\=A Ǟ !c{E} Ȩ !^\ain ᵜ !frac38 ⅜ !subseteqq ⊆ !^\lezh with retroflex hook 𐞟 +!Dd ⅅ !bF 𝔽 !BGS 𝚺 !Mfo 𝔬 !"\=O Ȫ !c{G} Ģ !^\chi ᵡ !frac45 ⅘ !subsetneq ⊊ !^\dotless j with stroke and hook 𐞘 +!F0 0 !bG 𝔾 !BGT 𝚻 !Mfp 𝔭 !"\=U Ǖ !c{H} Ḩ !^\eng ᵑ !frac56 ⅚ !supseteqq ⊇ !^\dz digraph with retroflex hook 𐞈 +!F1 1 !bH ℍ !BGU 𝚼 !Mfq 𝔮 !"\=a ǟ !c{K} Ķ !^\esh ᶴ !frac58 ⅝ !supsetneq ⊋ !^\l with retroflex hook and belt 𐞝 +!F2 2 !bI 𝕀 !BGX 𝚵 !Mfr 𝔯 !"\=o ȫ !c{L} Ļ !^\eth ᶞ !frac78 ⅞ !telephone ℡ !^\ts digraph with retroflex hook 𐞭 +!F3 3 !bJ 𝕁 !BGZ 𝚭 !Mfs 𝔰 !"\=u ǖ !c{N} Ņ !^\ezh ᶾ !gangia ϫ !textnaira ₦ !^\turned r with long leg and retroflex hook 𐞧 +!F4 4 !bK 𝕂 !BGa 𝛂 !Mft 𝔱 !"\`U Ǜ !c{R} Ŗ !^\phi ᶲ !gtrdot ⋗ !therefore ∴ +!F5 5 !bL 𝕃 !BGb 𝛃 !Mfu 𝔲 !"\`u ǜ !c{S} Ş !^{SM} ℠ !gtrsim ≳ !triangleq ≜ +!F6 6 !bM 𝕄 !BGc 𝛘 !Mfv 𝔳 !"\vU Ǚ !c{T} Ţ !^{TM} ™ !hsmash ⬌ !varpropto ∝ +!F7 7 !bN ℕ !BGd 𝛅 !Mfw 𝔴 !"\vu ǚ !c{c} ç !^{\j} ĵ !iiiint ⨌ !Lleftarrow ⇚ +!F8 8 !bO 𝕆 !BGe 𝛆 !Mfx 𝔵 !"{A} Ä !c{d} ḑ !above ┴ !k\={O} Ǭ !Rightarrow ⇒ +!F9 9 !bP ℙ !BGf 𝛗 !Mfy 𝔶 !"{E} Ë !c{e} ȩ !aleph ℵ !k\={o} ǭ !^\Barred B ᴯ +!FA A !bQ ℚ !BGg 𝛄 !Mfz 𝔷 !"{H} Ḧ !c{g} ģ !alpha α !kelvin K !^\barred o ᶱ +""" + +agda_words = extras + parse_source(source) + +if __name__ == "__main__": + print(f"{len(agda_words)} keybinds") diff --git a/kbtrans/generate_xkb.py b/kbtrans/generate_xkb.py new file mode 100644 index 0000000..dabc4b0 --- /dev/null +++ b/kbtrans/generate_xkb.py @@ -0,0 +1,144 @@ +from typing import Set + +import sys + +import kb +import kc +import mac_parser + +IN_PATH = sys.argv[1] +OUT_PATH = sys.argv[2] + +mac_kb = mac_parser.parse_keyboard_layout(IN_PATH) +keyboard = kb.from_mac(mac_kb) + +with open(OUT_PATH, "w") as out_file: + out_file.write("xkb_keymap {\n") + + out_file.write(' xkb_keycodes "mjaucodes"{\n') + for knr, name in kc.MAC_KEY_NAMES.items(): + key = kc.KeyCode(knr) + for lin_n, lin_name in key.linux_keycodes(): + out_file.write(f' {lin_name} = {lin_n}; // {key}\n') + + out_file.write(' = 22; // Backspace\n') + out_file.write(' = 50; // Left shift\n') + out_file.write(' = 62; // Right alt\n') + out_file.write(' = 64; // Left alt\n') + out_file.write(' = 108; // Right alt\n') + out_file.write(' = 9; // Escape\n') + out_file.write(' = 23; // Tab\n') + out_file.write(' = 36; // Return\n') + out_file.write(' = 37; // Left Control\n') + out_file.write(' = 105; // Right Control\n') + out_file.write(' = 111; // Arrow up\n') + out_file.write(' = 116; // Arrow down\n') + out_file.write(' = 113; // Arrow left\n') + out_file.write(' = 114; // Arrow right\n') + out_file.write(' = 66; // Caps lock\n') + out_file.write(' };\n') + out_file.write('''\ + xkb_types "mjautypes" { + virtual_modifiers NumLock,Alt,LevelThree,LAlt,RAlt,RControl,LControl,ScrollLock,LevelFive,AltGr,Meta,Super,Hyper; + + type "ONE_LEVEL" { + modifiers= none; + level_name[Level1]= "Any"; + }; + type "ALPHABETIC" { + modifiers= Shift+Lock; + map[Shift]= Level2; + map[Lock]= Level2; + level_name[Level1]= "Base"; + level_name[Level2]= "Caps"; + }; + type "ALTABLE" { + modifiers= Shift+Mod2; + map[Shift]= Level2; + map[Mod2]= Level3; + map[Shift+Mod2]= Level4; + level_name[Level1]= "Base"; + level_name[Level2]= "Shift"; + level_name[Level3]= "Alt Base"; + level_name[Level4]= "Shift Alt"; + }; + }; + xkb_compatibility "mjaucomp" { + virtual_modifiers NumLock,Alt,LevelThree,LAlt,RAlt,RControl,LControl,ScrollLock,LevelFive,AltGr,Meta,Super,Hyper; + interpret.useModMapMods= AnyLevel; + interpret.repeat= False; + interpret.locking= False; + interpret LSHF+AnyOfOrNone(all) { + action= SetMods(modifiers=Shift,clearLocks); + }; + interpret LSHF+AnyOfOrNone(all) { + action= SetMods(modifiers=Shift,clearLocks); + }; + interpret LALT+AnyOf(all) { + action= SetMods(modifiers=Mod2,clearLocks); + }; + interpret RALT+AnyOf(all) { + action= SetMods(modifiers=modMapMode,clearLocks); + }; + }; +''') + out_file.write('''\ + xkb_symbols "mjau"{ + key { [ Alt_L ] }; + key { [ Alt_R ] }; + + key { [ Shift_L ] }; + key { [ Shift_R ] }; + key { [ Control_L ] }; + key { [ Control_R ] }; + + key { [ BackSpace ] }; + key { [ Tab ] }; + key { [ Escape ] }; + key { [ Escape ] }; + key { [ Return ] }; + + key { [ Up ] }; + key { [ Down ] }; + key { [ Left ] }; + key { [ Right ] }; + + modifier_map Control { }; + modifier_map Control { }; + modifier_map Mod1 { }; + modifier_map Mod2 { }; +''') + + def get_output(layer: kb.Layer, kc: kb.KeyCode, mods: Set[kb.Modifier]) -> str: + key = kb.Key(kc, mods) + if key not in layer.buttons: + return "none" + press = layer.buttons[key] + if not isinstance(press, kb.Output): + return "none" + + if len(press.output) != 1: + print(f"Ignoring multi-output key {kc}+{mods}") + return "none" + + return "U" + hex(ord(press.output[0]))[2:].rjust(4, "0") + + layer = keyboard.layers[keyboard.default_layer] + for keycode in kc.KEYCODES: + none = get_output(layer, keycode, set()) + shifted = get_output(layer, keycode, set([kb.Modifier.SHIFT])) + alted = get_output(layer, keycode, set([kb.Modifier.OPTION])) + shalted = get_output(layer, keycode, set([kb.Modifier.SHIFT, kb.Modifier.OPTION])) + + if none == shifted == alted == shalted == "none": + continue + + for _, lin_name in keycode.linux_keycodes(): + out_file.write(f' key {lin_name} {{ type = "ALTABLE", symbols[Group1]= [ {none}, {shifted}, {alted}, {shalted} ] }}; // {keycode}\n') + + out_file.write(' };\n') + out_file.write(' xkb_geometry "mjauometry" {};\n') + + out_file.write("};\n") +print("Wrote to", OUT_PATH) + diff --git a/kbtrans/kb.py b/kbtrans/kb.py new file mode 100644 index 0000000..ec5e200 --- /dev/null +++ b/kbtrans/kb.py @@ -0,0 +1,143 @@ +# "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}") diff --git a/kbtrans/kc.py b/kbtrans/kc.py new file mode 100644 index 0000000..2a95064 --- /dev/null +++ b/kbtrans/kc.py @@ -0,0 +1,268 @@ +from typing import Set, Tuple, Any + +# From Events.h +MAC_KEY_NAMES = { + 0x00: "A", + 0x01: "S", + 0x02: "D", + 0x03: "F", + 0x04: "H", + 0x05: "G", + 0x06: "Z", + 0x07: "X", + 0x08: "C", + 0x09: "V", + 0x0A: "International", + 0x0B: "B", + 0x0C: "Q", + 0x0D: "W", + 0x0E: "E", + 0x0F: "R", + 0x10: "Y", + 0x11: "T", + 0x12: "1", + 0x13: "2", + 0x14: "3", + 0x15: "4", + 0x16: "6", + 0x17: "5", + 0x18: "Equal", + 0x19: "9", + 0x1A: "7", + 0x1B: "Minus", + 0x1C: "8", + 0x1D: "0", + 0x1E: "RightBracket", + 0x1F: "O", + 0x20: "U", + 0x21: "LeftBracket", + 0x22: "I", + 0x23: "P", + 0x25: "L", + 0x26: "J", + 0x27: "Quote", + 0x28: "K", + 0x29: "Semicolon", + 0x2A: "Backslash", + 0x2B: "Comma", + 0x2C: "Slash", + 0x2D: "N", + 0x2E: "M", + 0x2F: "Period", + 0x32: "Grave", + 0x41: "KPDecimal", + 0x43: "KPMultiply", + 0x45: "KPPlus", + 0x47: "KPClear", + 0x4B: "KPDivide", + 0x4C: "KPEnter", + 0x4E: "KPMinus", + 0x51: "KPEquals", + 0x52: "KP0", + 0x53: "KP1", + 0x54: "KP2", + 0x55: "KP3", + 0x56: "KP4", + 0x57: "KP5", + 0x58: "KP6", + 0x59: "KP7", + 0x5B: "KP8", + 0x5C: "KP9", + 0x24: "Return", + 0x30: "Tab", + 0x31: "Space", + 0x33: "Del", + 0x35: "Escape", + 0x37: "Command", + 0x38: "Shift", + 0x39: "CapsLock", + 0x3A: "Option", + 0x3B: "Control", + 0x3C: "RightShift", + 0x3D: "RightOption", + 0x3E: "RightControl", + 0x3F: "Function", + 0x40: "F17", + 0x48: "VolumeUp", + 0x49: "VolumeDown", + 0x4A: "Mute", + 0x4F: "F18", + 0x50: "F19", + 0x5A: "F20", + 0x60: "F5", + 0x61: "F6", + 0x62: "F7", + 0x63: "F3", + 0x64: "F8", + 0x65: "F9", + 0x67: "F11", + 0x69: "F13", + 0x6A: "F16", + 0x6B: "F14", + 0x6D: "F10", + 0x6F: "F12", + 0x71: "F15", + 0x72: "Help", + 0x73: "Home", + 0x74: "PageUp", + 0x75: "FwdDel", + 0x76: "F4", + 0x77: "End", + 0x78: "F2", + 0x79: "PageDown", + 0x7A: "F1", + # 0x7B: "LeftArrow", + # 0x7C: "RightArrow", + # 0x7D: "DownArrow", + # 0x7E: "UpArrow", +} + +# From https://gist.github.com/rickyzhang82/8581a762c9f9fc6ddb8390872552c250 +# Highly modified +LINUX_KEYCODE_NAMES = { + 9: "Escape", # Esc + 10: "1", # 1 + 11: "2", # 2 + 12: "3", # 3 + 13: "4", # 4 + 14: "5", # 5 + 15: "6", # 6 + 16: "7", # 7 + 17: "8", # 8 + 18: "9", # 9 + 19: "0", # 0 + 20: "Minus", # - + 21: "Equal", # = + 22: "Backspace", # Backspace + 23: "Tab", # Tab + 24: "Q", # Q + 25: "W", # W + 26: "E", # E + 27: "R", # R + 28: "T", # T + 29: "Y", # Y + 30: "U", # U + 31: "I", # I + 32: "O", # O + 33: "P", # P + 34: "LeftBracket", # [ + 35: "RightBracket", # ] + 36: "Return", # Return + 37: "Control", # Ctrl Left + 38: "A", # A + 39: "S", # S + 40: "D", # D + 41: "F", # F + 42: "G", # G + 43: "H", # H + 44: "J", # J + 45: "K", # K + 46: "L", # L + 47: "Semicolon", # ; + 48: "Quote", # ' + 49: "International", # ` + 50: "Shift", # Shift Left + 51: "Backslash", # \ + 52: "Z", # Z + 53: "X", # X + 54: "C", # C + 55: "V", # V + 56: "B", # B + 57: "N", # N + 58: "M", # M + 59: "Comma", # , + 60: "Period", # . + 61: "Slash", # / + 62: "RightShift", # Shift Right + 63: "KPMultiply", # KP * + 64: "Option", # Alt Left (-> Command) + 65: "Space", # Space + 66: "CapsLock", # Caps Lock + 67: "F1", # F1 + 68: "F2", # F2 + 69: "F3", # F3 + 70: "F4", # F4 + 71: "F5", # F5 + 72: "F6", # F6 + 73: "F7", # F7 + 74: "F8", # F8 + 75: "F9", # F9 + 76: "F10", # F10 + # 77: "Num Lock", # Num Lock + # 78: "Scroll Lock", # Scroll Lock + 79: "KP7", # KP 7 + 80: "KP8", # KP 8 + 81: "KP9", # KP 9 + 82: "KPMinus", # KP - + 83: "KP4", # KP 4 + 84: "KP5", # KP 5 + 85: "KP6", # KP 6 + 86: "KPPlus", # KP + + 87: "KP1", # KP 1 + 88: "KP2", # KP 2 + 89: "KP3", # KP 3 + 90: "KP0", # KP 0 + 91: "KPDecimal", # KP . + 94: "Grave", # International + 95: "F11", # F11 + 96: "F12", # F12 + 97: "Home", # Home + 98: "UpArrow", # Cursor Up + 99: "PageUp", # Page Up + 100: "LeftArrow", # Cursor Left + 102: "RightArrow", # Cursor Right + 103: "End", # End + 104: "DownArrow", # Cursor Down + # 105: "PageDown", # Page Down + # 106: "Insert", # Insert + 107: "Del", # Delete + 108: "KPEnter", # KP Enter + # 109: "RightControl", # Ctrl Right + # 110: "Pause", # Pause + # 111: "PrintScrn", # PrintScrn + 112: "KPDivide", # KP / + 113: "RightOption", # Alt Right (-> Command) + 115: "Command", # Logo Left (-> Option) + 116: "Command", # Logo Right (-> Option) + # 117: "Menu (-> International)", # Menu (-> International) +} + +# Represenetd by the Mac virtual keycodes +class KeyCode: + def __init__(self, kc: int): + self.kc = kc + + def __eq__(self, other: Any) -> bool: + if isinstance(other, KeyCode): + return self.kc == other.kc + return False + + def __hash__(self) -> int: + return hash(self.kc) ^ 1 + + def __str__(self) -> str: + if self.kc in MAC_KEY_NAMES: + return MAC_KEY_NAMES[self.kc] + else: + return f"mac_{self.kc}" + + __repr__ = __str__ + + def linux_keycodes(self) -> Set[Tuple[int, str]]: + if self.kc not in MAC_KEY_NAMES: + return set() + + res = set() + for n, name in LINUX_KEYCODE_NAMES.items(): + if name == MAC_KEY_NAMES[self.kc]: + res.add((n, f"")) + + return res + +KEYCODES = [KeyCode(x) for x in MAC_KEY_NAMES.keys()] + +if __name__ == "__main__": + for n, name in MAC_KEY_NAMES.items(): + linuxs = KeyCode(n).linux_keycodes() + if len(linuxs) == 0: + print(f"{name} has no linux variant...") diff --git a/kbtrans/mac_parser.py b/kbtrans/mac_parser.py new file mode 100644 index 0000000..adc355b --- /dev/null +++ b/kbtrans/mac_parser.py @@ -0,0 +1,330 @@ +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 diff --git a/kbtrans/mackb.py b/kbtrans/mackb.py new file mode 100644 index 0000000..4737424 --- /dev/null +++ b/kbtrans/mackb.py @@ -0,0 +1,93 @@ +# Quite literal representation of the macOS .keylayout file +from __future__ import annotations + +from typing import Dict, List, NewType, Set +from abc import ABC +from enum import Enum + +from kb import KeyCode, Modifier +from kc import KeyCode + +StateName = NewType("StateName", str) +ActionID = NewType("ActionID", str) +MapIndex = NewType("MapIndex", int) + +NO_STATE = StateName("none") + +class State: + def __init__( + self, + terminator: str, + ): + self.terminator = terminator + +class ModMap: + def __init__( + self, + selects: Dict[MapIndex, Set[Modifier]], + ): + self.selects = selects + + def __repr__(self) -> str: + return f"ModMap(selects={self.selects})" + +class KeyMap: + def __init__( + self, + keys: Dict[KeyCode, OnPress], + ): + self.keys = keys + + def __repr__(self) -> str: + return f"KeyMap(keys={self.keys})" + +class OnPress(ABC): # the action, output or next attribute of a or node + pass + +class ActionReference(OnPress): # <... action="..." /> + def __init__(self, ref: ActionID): + self.ref = ref + + def __repr__(self) -> str: + return f"ActionReference({self.ref!r})" + +class Output(OnPress): # <... output="..." /> + def __init__(self, output: str): + self.output = output + + def __repr__(self) -> str: + return f"Output({self.output!r})" + +class EnterState(OnPress): # <... next="..." /> + def __init__(self, state_name: StateName): + self.state_name = state_name + + def __repr__(self) -> str: + return f"EnterState({self.state_name!r})" + +class Action: + def __init__( + self, + state_actions: Dict[StateName, OnPress], + ): + self.state_actions = state_actions + + def __repr__(self) -> str: + return f"Action({self.state_actions})" + +class MacKeyboardLayout: + def __init__( + self, + name: str, + + states: Dict[StateName, State], + modmap: ModMap, + keymaps: Dict[MapIndex, KeyMap], + actions: Dict[ActionID, Action], + ): + self.name = name + + self.states = states + self.modmap = modmap + self.keymaps = keymaps + self.actions = actions diff --git a/svorak.keylayout b/svorak.keylayout new file mode 100644 index 0000000..44ce3ff --- /dev/null +++ b/svorak.keylayout @@ -0,0 +1,1296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +