fix(text-injection): end option parsing before typed text (#922)
* fix(text-injection): end option parsing before typed text A 20-char xdotool chunk, or a 200-char wtype/ydotool chunk, that starts with '-' (a word hyphenated across the chunk boundary) is parsed as a flag: 'xdotool type --clearmodifiers -stickation' exits 1 with 'type: unrecognized option' and the rest of the dictation is dropped. Pass '--' before the text argument in the xdotool, wtype, and ydotool type invocations so the remaining argument is always typed literally. Fixes #921 * test(portal-fallback): expect the wtype option terminator The partial-type fallback assertion predates the -- terminator and still expected the bare ['wtype', 'llo'] argv. Refs #921 --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
co-authored by
Jatin K Malik
parent
d021e64813
commit
295386f08b
@@ -1861,8 +1861,12 @@ class TextInjector:
|
||||
chunk = text[i : i + chunk_size]
|
||||
chunk_num = (i // chunk_size) + 1
|
||||
|
||||
# First try with clearmodifiers
|
||||
cmd = ["xdotool", "type", "--clearmodifiers", chunk]
|
||||
# First try with clearmodifiers. ``--`` ends option
|
||||
# parsing: a chunk that starts with '-' (a word
|
||||
# hyphenated across the chunk boundary) is typed
|
||||
# literally instead of read as a flag and rejected
|
||||
# (#921).
|
||||
cmd = ["xdotool", "type", "--clearmodifiers", "--", chunk]
|
||||
logger.debug(f"Injecting chunk {chunk_num}/{total_chunks}: '{chunk}'")
|
||||
|
||||
subprocess.run(
|
||||
@@ -2660,7 +2664,9 @@ class TextInjector:
|
||||
raise _InjectionAborted
|
||||
chunk = text[i : i + chunk_size]
|
||||
if self.wayland_tool == "wtype":
|
||||
cmd = ["wtype", chunk]
|
||||
# ``--`` puts wtype in raw-text mode so a leading '-' is not
|
||||
# parsed as an option.
|
||||
cmd = ["wtype", "--", chunk]
|
||||
# wtype types at compositor pace; the budget scales with the
|
||||
# chunk length so only a wedged process can ever hit it.
|
||||
type_timeout = max(5, len(chunk) * 0.05)
|
||||
@@ -2668,7 +2674,8 @@ class TextInjector:
|
||||
# Keep key-delay > 0 to avoid Shift-leak ("Can you" -> "CAN YOu").
|
||||
# Low delay so fallback typing finishes quickly for long phrases.
|
||||
key_delay = os.environ.get("VOCALINUX_YDOTOOL_KEY_DELAY", "2")
|
||||
cmd = ["ydotool", "type", "--key-delay", key_delay, chunk]
|
||||
# ``--`` ends ydotool's option parsing for the same reason.
|
||||
cmd = ["ydotool", "type", "--key-delay", key_delay, "--", chunk]
|
||||
type_timeout = max(5, len(chunk) * self._key_delay_seconds(key_delay) * 4)
|
||||
try:
|
||||
subprocess.run(
|
||||
|
||||
@@ -695,7 +695,7 @@ class TestPortalInjectionPaths:
|
||||
):
|
||||
injector._inject_with_wayland_tool("hello")
|
||||
# The portal typed "he" before failing; the fallback gets only "llo".
|
||||
assert mock_run.call_args[0][0] == ["wtype", "llo"]
|
||||
assert mock_run.call_args[0][0] == ["wtype", "--", "llo"]
|
||||
|
||||
def test_backspace_retries_only_remaining_taps(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
injector = _bare_injector(environment=DesktopEnvironment.WAYLAND, wayland_tool="portal")
|
||||
|
||||
@@ -271,7 +271,7 @@ class TestTextInjector(unittest.TestCase):
|
||||
|
||||
# Verify wtype was called correctly
|
||||
self.mock_subprocess.assert_any_call(
|
||||
["wtype", "Hello world"],
|
||||
["wtype", "--", "Hello world"],
|
||||
check=True,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
@@ -299,7 +299,7 @@ class TestTextInjector(unittest.TestCase):
|
||||
# Clipboard paste fails (no wl-copy/xclip/xsel in this mock); type fallback.
|
||||
# Default key-delay is 2 (overridable via VOCALINUX_YDOTOOL_KEY_DELAY).
|
||||
self.mock_subprocess.assert_any_call(
|
||||
["ydotool", "type", "--key-delay", "2", "Hello world"],
|
||||
["ydotool", "type", "--key-delay", "2", "--", "Hello world"],
|
||||
check=True,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
@@ -820,7 +820,7 @@ class TestTextInjector(unittest.TestCase):
|
||||
|
||||
calls = [c.args[0] for c in mock_run.call_args_list if c.args]
|
||||
self.assertTrue(
|
||||
any(c[0] == "wtype" and c[1] == "café" for c in calls),
|
||||
any(c[0] == "wtype" and c[-1] == "café" for c in calls),
|
||||
"wtype should inject text directly",
|
||||
)
|
||||
self.assertFalse(
|
||||
@@ -1786,6 +1786,67 @@ class TestTextInjectorEdgeCases(unittest.TestCase):
|
||||
)
|
||||
self.assertFalse(any("Escape" in command for command in commands))
|
||||
|
||||
def test_inject_with_xdotool_terminates_options_before_the_text(self) -> None:
|
||||
"""A chunk starting with '-' must type literally, not parse as a flag (#921).
|
||||
|
||||
Text is typed in 20-char chunks, so a word hyphenated across the
|
||||
boundary produces a chunk like "-stickation". Without the end-of-options
|
||||
marker `xdotool type` reads it as a flag, exits nonzero, and drops the
|
||||
rest of the dictation.
|
||||
"""
|
||||
injector = TextInjector.__new__(TextInjector)
|
||||
injector.environment = DesktopEnvironment.X11
|
||||
|
||||
injector._inject_with_xdotool("this is a hyphenated-stickation")
|
||||
|
||||
type_calls = [
|
||||
call.args[0]
|
||||
for call in self.mock_subprocess.call_args_list
|
||||
if call.args and call.args[0][:2] == ["xdotool", "type"]
|
||||
]
|
||||
self.assertEqual(
|
||||
type_calls,
|
||||
[
|
||||
["xdotool", "type", "--clearmodifiers", "--", "this is a hyphenated"],
|
||||
["xdotool", "type", "--clearmodifiers", "--", "-stickation"],
|
||||
],
|
||||
)
|
||||
|
||||
def test_inject_with_wtype_terminates_options_before_the_text(self) -> None:
|
||||
"""`wtype` gets the same marker: its raw-text mode types the rest (#921)."""
|
||||
injector = TextInjector.__new__(TextInjector)
|
||||
injector.wayland_tool = "wtype"
|
||||
injector._wait_for_modifiers_released = lambda: None
|
||||
|
||||
injector._inject_with_wayland_tool("-leading dash")
|
||||
|
||||
type_calls = [
|
||||
call.args[0]
|
||||
for call in self.mock_subprocess.call_args_list
|
||||
if call.args and call.args[0][:1] == ["wtype"]
|
||||
]
|
||||
self.assertEqual(type_calls, [["wtype", "--", "-leading dash"]])
|
||||
|
||||
def test_inject_with_ydotool_terminates_options_before_the_text(self) -> None:
|
||||
"""`ydotool type` stops option parsing before the chunk as well (#921)."""
|
||||
injector = TextInjector.__new__(TextInjector)
|
||||
injector.wayland_tool = "ydotool"
|
||||
injector._wait_for_modifiers_released = lambda: None
|
||||
injector._ensure_ydotoold = lambda: True
|
||||
injector._inject_via_clipboard_paste = lambda text, **kwargs: False
|
||||
|
||||
injector._inject_with_wayland_tool("-leading dash")
|
||||
|
||||
type_calls = [
|
||||
call.args[0]
|
||||
for call in self.mock_subprocess.call_args_list
|
||||
if call.args and call.args[0][:2] == ["ydotool", "type"]
|
||||
]
|
||||
self.assertEqual(len(type_calls), 1)
|
||||
cmd = type_calls[0]
|
||||
self.assertEqual(cmd[:3], ["ydotool", "type", "--key-delay"])
|
||||
self.assertEqual(cmd[-2:], ["--", "-leading dash"])
|
||||
|
||||
def test_inject_with_wayland_tool_ydotool(self):
|
||||
"""Test text injection with ydotool."""
|
||||
with patch.dict("os.environ", {"XDG_SESSION_TYPE": "wayland"}):
|
||||
|
||||
Reference in New Issue
Block a user