extension.py

Pinchord · pinchord.git · Tickets · Changelog

Branches > serial-mods > latest > files / plover_pinchord_31 / extension.py

## Makes the affix keys work on every entry, even user-defined ones
import re
import sys
from plover import system
from plover.steno import Stroke

PREFIXES = {(): "{}",
            ("#-",): "{}{-|}",
            ("+-",): "{^}",
            ("#-", "+-"): "{^}{-|}"}

SUFFIXES = {(): "",
            ("-e",): "{^~|^}e",
            ("-s",): "{^~|^}s",
            ("-e", "-s"): "{^~|^}es"}

# Macros don't work with translations like {} (only =undo works, not {}=undo)
MACRO = r"=\w+(:.*)?|\{\*[!?+]?\}"

# Wraps the given translation in its affix translations
# e.g. with_affixes(Stroke("#"), "test", Stroke("-s")) -> "{}{-|}test{^~|^}s"
def with_affixes(prefixes, translation, suffixes):
  if translation is None:
    return None
  elif re.fullmatch(MACRO, translation):
    return None if prefixes or suffixes else translation
  else:
    return PREFIXES[prefixes.keys()] + translation + SUFFIXES[suffixes.keys()]

# Looks up the stroke in the dictionary stack after splitting the affix keys,
# returns the translation joined with the affix key translations
def affixed_lookup(lookup, stroke):
  prefixes = stroke & Stroke("#+")
  suffixes = stroke & Stroke("-es")
  stroke_without_affixes = stroke - prefixes - suffixes
  stroke_entry = lookup((stroke.rtfcre,))

  if stroke_entry is not None and stroke != stroke_without_affixes:
    return stroke_entry
  elif not stroke_without_affixes and suffixes:
    return with_affixes(prefixes, "", suffixes)
  else:
    return with_affixes(prefixes, lookup((stroke_without_affixes.rtfcre,)), suffixes)

engine = None

class Extension:
  def __init__(self, engine):
    self.dictionaries = engine.dictionaries
    self.original_lookup = self.dictionaries.lookup
    self.engine = engine

  def start(self):
    self.dictionaries.lookup = self.lookup
    global engine
    engine = self.engine

  def stop(self):
    self.dictionaries.lookup = self.original_lookup
    global engine
    engine = None

  def lookup(self, outline):
      # inlines the system name, be aware!
    if system.NAME == "Pinchord" and len(outline) == 1:
      return affixed_lookup(self.original_lookup, Stroke.from_steno(outline[0]))
    else:
      return self.original_lookup(outline)

def thread_unsafe_press_or_release(mods, engine, pressed_boolean):
  # Grabs the parse_key_combo function from the engine's keyboard emulation module
  # Replaces it in this call with a version that strips the press or release part
  # send_key_combo calls this function, and presses and releases everything it returns
  # So the press or release part is stripped out
  emulation = engine._keyboard_emulation
  emulation_module = sys.modules[type(emulation).__module__]
  original_parse_key_combo = emulation_module.parse_key_combo
  emulation_module.parse_key_combo = lambda *args: [event for event in original_parse_key_combo(*args)
                                                    if event[1] == pressed_boolean]
  try:
    emulation.send_key_combo(" ".join(mods))
  finally:
    emulation_module.parse_key_combo = original_parse_key_combo

def do_press_or_release(mods, press):
  global engine
  if mods and engine:
    engine._same_thread_hook(thread_unsafe_press_or_release, mods, engine, press)

def press(mods: tuple[str]):
  """Starts pressing down a tuple of mods (like "control", "shift") without releasing them."""
  do_press_or_release(mods, True)

def release(mods):
  """Releases the tuple of mods (like "control", "shift") without pressing them first."""
  do_press_or_release(mods, False)