From b267f0cd684ec1418d37b4bdf8b19dad7eed084d Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Fri, 4 Nov 2022 10:01:16 +1100 Subject: [PATCH] Cleanup: isolate input simulation into self contained functions Input simulation was becoming difficult to follow, now each simulated input method is a separate function. --- nerd-dictation | 232 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 167 insertions(+), 65 deletions(-) diff --git a/nerd-dictation b/nerd-dictation index 93e3412..3b6b3df 100755 --- a/nerd-dictation +++ b/nerd-dictation @@ -37,6 +37,8 @@ USER_CONFIG_DIR = "nerd-dictation" USER_CONFIG = "nerd-dictation.py" +SIMULATE_INPUT_CODE_COMMAND = -1 + # ----------------------------------------------------------------------------- # General Utilities @@ -53,42 +55,6 @@ def run_command_or_exit_on_failure(cmd: List[str]) -> None: sys.exit(1) -def simulate_backspace_presses(count: int, simulate_input_tool: str) -> None: - cmd = [simulate_input_tool.lower()] - if simulate_input_tool == "XDOTOOL": - cmd += ["key", "--"] + ["BackSpace"] * count - elif simulate_input_tool == "YDOTOOL": - # ydotool's key subcommand works with int key IDs and key states. 14 is - # the linux keycode for the backspace key, and :1 and :0 respectively - # stand for "pressed" and "released." - # - # The key delay is lower than the typing setting because it applies to - # each key state change (pressed, released). - cmd += [ - "key", - "--key-delay", - "3", - "--", - ] + ["14:1", "14:0"] * count - elif simulate_input_tool == "WTYPE": - # Without the delay (5ms) the backspace does not work correctly every time - cmd += ["-s", "5"] + ["-k", "backSpace"] * count - run_command_or_exit_on_failure(cmd) - - -def simulate_typing(text: str, simulate_input_tool: str) -> None: - cmd = [simulate_input_tool.lower()] - if simulate_input_tool == "XDOTOOL": - cmd += ["type", "--clearmodifiers", "--", text] - elif simulate_input_tool == "YDOTOOL": - # The low delay value makes typing fast, making the output much snappier - # than the slow default. - cmd += ["type", "--next-delay", "5", "--", text] - elif simulate_input_tool == "WTYPE": - cmd += [text] - run_command_or_exit_on_failure(cmd) - - def touch(filepath: str, mtime: Optional[float] = None) -> None: if os.path.exists(filepath): os.utime(filepath, None if mtime is None else (mtime, mtime)) @@ -167,6 +133,147 @@ def execfile(filepath: str, mod: Optional[ModuleType] = None) -> Optional[Module return mod +# ----------------------------------------------------------------------------- +# Simulate Input: XDOTOOL +# +def simulate_typing_with_xdotool(delete_prev_chars: int, text: str) -> None: + cmd = "xdotool" + + # No setup/tear-down. + if delete_prev_chars == SIMULATE_INPUT_CODE_COMMAND: + return + + if delete_prev_chars: + run_command_or_exit_on_failure( + [ + cmd, + "key", + "--", + *(["BackSpace"] * delete_prev_chars), + ] + ) + + run_command_or_exit_on_failure( + [ + cmd, + "type", + "--clearmodifiers", + "--", + text, + ] + ) + + +# ----------------------------------------------------------------------------- +# Simulate Input: YDOTOOL +# +def simulate_typing_with_ydotool(delete_prev_chars: int, text: str) -> None: + cmd = "ydotool" + + # No setup/tear-down. + if delete_prev_chars == SIMULATE_INPUT_CODE_COMMAND: + return + + if delete_prev_chars: + # ydotool's key subcommand works with int key IDs and key states. 14 is + # the linux keycode for the backspace key, and :1 and :0 respectively + # stand for "pressed" and "released." + # + # The key delay is lower than the typing setting because it applies to + # each key state change (pressed, released). + run_command_or_exit_on_failure( + [ + cmd, + "key", + "--key-delay", + "3", + "--", + *(["14:1", "14:0"] * delete_prev_chars), + ] + ) + + # The low delay value makes typing fast, making the output much snappier + # than the slow default. + run_command_or_exit_on_failure( + [ + cmd, + "type", + "--next-delay", + "5", + "--", + text, + ] + ) + + +# ----------------------------------------------------------------------------- +# Simulate Input: DOTOOL +# +def simulate_typing_with_dotool(delete_prev_chars: int, text: str) -> None: + cmd = "dotool" + + if delete_prev_chars == SIMULATE_INPUT_CODE_COMMAND: + global simulate_typing_with_dotool_proc + if text == "SETUP": + # If this isn't true, something strange is going on. + assert simulate_typing_with_dotool_proc is None + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, text=True) + assert proc.stdin is not None + proc.stdin.write("keydelay 4\ntypedelay 12\n") + proc.stdin.flush() + simulate_typing_with_dotool_proc = proc + elif text == "TEARDOWN": + import signal + assert simulate_typing_with_dotool_proc is not None + os.kill(simulate_typing_with_dotool_proc.pid, signal.SIGINT) + # Not needed, just basic hygiene not to keep killed process reference. + simulate_typing_with_dotool_proc = None + else: + raise Exception("Internal error, unknown command {!r}".format(text)) + return + + assert simulate_typing_with_dotool_proc is not None + proc = simulate_typing_with_dotool_proc + assert proc.stdin is not None + if delete_prev_chars: + print(" backspace" * delete_prev_chars) + proc.stdin.write("key" + (" backspace" * delete_prev_chars) + "\n") + proc.stdin.flush() + + proc.stdin.write("type " + text + "\n") + proc.stdin.flush() + + +simulate_typing_with_dotool_proc = None + +# ----------------------------------------------------------------------------- +# Simulate Input: WTYPE +# +def simulate_typing_with_wtype(delete_prev_chars: int, text: str) -> None: + cmd = "wtype" + + # No setup/tear-down. + if delete_prev_chars == SIMULATE_INPUT_CODE_COMMAND: + return + + if delete_prev_chars: + run_command_or_exit_on_failure( + [ + cmd, + "-s", + "5", + *(["-k", "backSpace"] * delete_prev_chars), + ] + ) + + run_command_or_exit_on_failure( + [ + cmd, + text, + ] + ) + + # ----------------------------------------------------------------------------- # Custom Configuration # @@ -719,7 +826,7 @@ def text_from_vosk_pipe( vosk_model_dir: str, exit_fn: Callable[..., int], process_fn: Callable[[str], str], - handle_fn: Callable[[str, int], None], + handle_fn: Callable[[int, str], None], timeout: float, idle_time: float, progressive: bool, @@ -833,7 +940,7 @@ def text_from_vosk_pipe( break # Emit text, deleting any previous incorrectly transcribed output - handle_fn(text_curr[match:], len(text_prev) - match) + handle_fn(len(text_prev) - match, text_curr[match:]) text_prev = text_curr @@ -845,6 +952,9 @@ def text_from_vosk_pipe( handled_any = True + # Support setting up input simulation state. + handle_fn(SIMULATE_INPUT_CODE_COMMAND, "SETUP") + # Use code to delay exiting, allowing reading the recording buffer to catch-up. code = 0 @@ -910,6 +1020,9 @@ def text_from_vosk_pipe( os.kill(ps.pid, signal.SIGINT) + # Support setting up input simulation state. + handle_fn(SIMULATE_INPUT_CODE_COMMAND, "TEARDOWN") + if code == -1: sys.stderr.write("Text input canceled!\n") sys.exit(0) @@ -924,7 +1037,7 @@ def text_from_vosk_pipe( if not progressive: # We never arrive here needing deletions - handle_fn(process_fn(" ".join(text_list)), 0) + handle_fn(0, process_fn(" ".join(text_list))) return handled_any @@ -1061,33 +1174,24 @@ def main_begin( # if output == "SIMULATE_INPUT": - if simulate_input_tool == "DOTOOL": - dotool = subprocess.Popen("dotool", stdin=subprocess.PIPE, text=True) - assert dotool.stdin is not None - dotool.stdin.write("keydelay 4\ntypedelay 12\n") - dotool.stdin.flush() - - def handle_fn(text: str, delete_prev_chars: int) -> None: - assert dotool.stdin is not None - if delete_prev_chars: - dotool.stdin.write("key" + " backspace" * delete_prev_chars + "\n") - dotool.stdin.flush() - dotool.stdin.write("type " + text + "\n") - dotool.stdin.flush() - + if simulate_input_tool == "XDOTOOL": + handle_fn = simulate_typing_with_xdotool + elif simulate_input_tool == "YDOTOOL": + handle_fn = simulate_typing_with_ydotool + elif simulate_input_tool == "DOTOOL": + handle_fn = simulate_typing_with_dotool + elif simulate_input_tool == "WTYPE": + handle_fn = simulate_typing_with_wtype else: - - def handle_fn(text: str, delete_prev_chars: int) -> None: - if delete_prev_chars: - # Backspace keycode. In ydotool we need to mark the key pressed with :1, then release it with :0 - # The default delay between keypresses is long and key delay stands for one key state change, - # so 3 ms == 6 ms to press and release backspace once. - simulate_backspace_presses(delete_prev_chars, simulate_input_tool) - simulate_typing(text, simulate_input_tool) + raise Exception("Internal error, unknown input tool: {!r}".format(simulate_input_tool)) elif output == "STDOUT": - def handle_fn(text: str, delete_prev_chars: int) -> None: + def handle_fn(delete_prev_chars: int, text: str) -> None: + # No setup/tear-down. + if delete_prev_chars == SIMULATE_INPUT_CODE_COMMAND: + return + if delete_prev_chars: sys.stdout.write("\x08" * delete_prev_chars) sys.stdout.write(text) @@ -1430,9 +1534,7 @@ def argparse_create_cancel(subparsers: argparse._SubParsersAction) -> None: subparse = subparsers.add_parser( "cancel", help="Cancel dictation.", - description=( - "This cancels dictation." - ), + description="This cancels dictation.", formatter_class=argparse.RawTextHelpFormatter, )