kblayouts/kbtrans/agda_input_method.py
2023-11-09 14:16:16 +01:00

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