kmk_firmware/kmk/modules/text_replacement.py
2022-07-14 10:29:25 +00:00

155 lines
5.2 KiB
Python

from kmk.keys import KC
from kmk.modules import Module
class State:
LISTENING = 0
DELETING = 1
SENDING = 2
# this class exists as an easy way to compare keys in a manner where
# a right-shifted key would be equivalent to a left-shifted key
class Character:
is_shifted = False
def __init__(self, key_code, is_shifted) -> None:
self.is_shifted = is_shifted
self.key_code = KC.LSHIFT(key_code) if is_shifted else key_code
def __eq__(self, other):
return (
self.key_code.code == other.key_code.code
and self.is_shifted == other.is_shifted
)
class Phrase:
def __init__(self, characters) -> None:
self._characters = characters
self._index = 0
def next_character(self):
character = self._characters[self._index]
self._index += 1
return character
def reset(self):
self._index = 0
def index_at_end(self):
return self._index == len(self._characters)
def character_is_next(self, character):
return self._characters[self._index] == character
class Rule:
def __init__(self, to_substitute, substitution) -> None:
self.to_substitute = to_substitute
self.substitution = substitution
def restart(self):
self.to_substitute.reset()
self.substitution.reset()
class TextReplacement(Module):
_shifted = False
_rules = []
_state = State.LISTENING
_matched_rule = None
def __init__(
self,
dictionary,
):
for entry in dictionary:
to_substitute = []
substitution = []
for char in entry:
key_code = getattr(KC, char.upper())
shifted = char.isupper() or key_code.has_modifiers == {2}
to_substitute.append(Character(key_code, shifted))
for char in dictionary[entry]:
key_code = getattr(KC, char.upper())
shifted = char.isupper() or key_code.has_modifiers == {2}
shifted = char.isupper()
substitution.append(Character(key_code, shifted))
self._rules.append(Rule(Phrase(to_substitute), Phrase(substitution)))
def process_key(self, keyboard, key, is_pressed, int_coord):
if not self._state == State.LISTENING:
return
if key is KC.LSFT or key is KC.RSFT:
if is_pressed:
self._shifted = True
else:
self._shifted = False
elif is_pressed:
character = Character(key, self._shifted)
# run through the dictionary to check for a possible match on each new keypress
for rule in self._rules:
if rule.to_substitute.character_is_next(character):
rule.to_substitute.next_character()
else:
rule.restart()
# if character is not a match at the current index,
# it could still be a match at the start of the sequence
# so redo the check after resetting the sequence
if rule.to_substitute.character_is_next(character):
rule.to_substitute.next_character()
# we've matched all of the characters in a phrase to be substituted
if rule.to_substitute.index_at_end():
rule.restart()
self._matched_rule = rule
self._state = State.DELETING
# if we have a match there's no reason to continue the full key processing, so return out
return
return super().process_key(keyboard, key, is_pressed, int_coord)
def during_bootup(self, keyboard):
return
def before_matrix_scan(self, keyboard):
return
def before_hid_send(self, keyboard):
if self._state == State.LISTENING:
return
if self._state == State.DELETING:
# send backspace taps equivalent to the length of the phrase to be substituted
to_substitute = self._matched_rule.to_substitute
to_substitute.next_character()
if not to_substitute.index_at_end():
keyboard.tap_key(KC.BSPC)
else:
self._state = State.SENDING
# if the user is holding shift, force-release it so that it doesn't modify the string to be sent
keyboard.remove_key(KC.LSFT)
keyboard.remove_key(KC.RSFT)
if self._state == State.SENDING:
substitution = self._matched_rule.substitution
if not substitution.index_at_end():
keyboard.tap_key(substitution.next_character().key_code)
else:
self._state = State.LISTENING
self._matched_rule = None
for rule in self._rules:
rule.restart()
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return
def after_matrix_scan(self, keyboard):
return