248 lines
8.2 KiB
Python
248 lines
8.2 KiB
Python
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")
|