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")