Skip to content

Commit

Permalink
Cleanup: isolate input simulation into self contained functions
Browse files Browse the repository at this point in the history
Input simulation was becoming difficult to follow, now each simulated
input method is a separate function.
  • Loading branch information
ideasman42 committed Nov 3, 2022
1 parent 29434d9 commit b267f0c
Showing 1 changed file with 167 additions and 65 deletions.
232 changes: 167 additions & 65 deletions nerd-dictation
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ USER_CONFIG_DIR = "nerd-dictation"

USER_CONFIG = "nerd-dictation.py"

SIMULATE_INPUT_CODE_COMMAND = -1


# -----------------------------------------------------------------------------
# General Utilities
Expand All @@ -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))
Expand Down Expand Up @@ -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
#
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
)

Expand Down

0 comments on commit b267f0c

Please sign in to comment.