Fix input forwarding: special chars, mouse events, and typing latency
Keyboard: - Use xdotool type for printable characters (handles @, #, !, etc.) - Use xdotool key only for non-printable keys and modifier combos - Remove --sync from every keystroke — focus set once, not per-event - Skip release events (xdotool key already handles press+release) Mouse: - Remove windowactivate --sync from every click — focus set once - Fire-and-forget for mousemove events to reduce latency - Add _xdotool_fire() for non-blocking subprocess calls Performance: - _ensure_focus() activates window once, skips on subsequent calls - Eliminated redundant --sync flags that added 100-300ms per event - Added _reap() helper to prevent zombie processes from fire-and-forget Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
12c10d69f0
commit
8d2a228599
1 changed files with 70 additions and 17 deletions
|
|
@ -372,6 +372,9 @@ class ManagedGuiApp:
|
|||
await self._xdotool("windowmove", "--sync", wid, "0", "0", env=env)
|
||||
# Resize to fill display (Xvfb is 1280x1024)
|
||||
await self._xdotool("windowsize", "--sync", wid, "1280", "1024", env=env)
|
||||
# Focus the window for input
|
||||
await self._xdotool("windowfocus", wid, env=env)
|
||||
self._focused = True
|
||||
# Some apps need a moment to redraw after resize
|
||||
await asyncio.sleep(0.3)
|
||||
log.debug("Maximized window %s to 1280x1024 for %s", wid, self.app_id)
|
||||
|
|
@ -397,6 +400,16 @@ class ManagedGuiApp:
|
|||
|
||||
# ---- Input forwarding ----
|
||||
|
||||
_focused: bool = False # tracks whether we've activated the window
|
||||
|
||||
async def _ensure_focus(self, env: dict):
|
||||
"""Activate/focus the window once, then skip on subsequent calls."""
|
||||
if not self._focused:
|
||||
wid = str(self.window_id)
|
||||
await self._xdotool("windowactivate", wid, env=env)
|
||||
await self._xdotool("windowfocus", wid, env=env)
|
||||
self._focused = True
|
||||
|
||||
async def send_input(self, msg: dict):
|
||||
"""Forward mouse/keyboard input to the X11 window."""
|
||||
if not self.window_id or not XDOTOOL_BIN:
|
||||
|
|
@ -418,23 +431,23 @@ class ManagedGuiApp:
|
|||
wid = str(self.window_id)
|
||||
|
||||
if action == "click":
|
||||
await self._ensure_focus(env)
|
||||
btn = str(msg.get("button", 1))
|
||||
await self._xdotool(
|
||||
"windowactivate", "--sync", wid,
|
||||
"mousemove", "--window", wid, x, y,
|
||||
"click", "--window", wid, btn,
|
||||
env=env,
|
||||
)
|
||||
elif action == "dblclick":
|
||||
await self._ensure_focus(env)
|
||||
btn = str(msg.get("button", 1))
|
||||
await self._xdotool(
|
||||
"windowactivate", "--sync", wid,
|
||||
"mousemove", "--window", wid, x, y,
|
||||
"click", "--window", wid, "--repeat", "2", btn,
|
||||
env=env,
|
||||
)
|
||||
elif action == "move":
|
||||
await self._xdotool(
|
||||
await self._xdotool_fire(
|
||||
"mousemove", "--window", wid, x, y,
|
||||
env=env,
|
||||
)
|
||||
|
|
@ -452,34 +465,52 @@ class ManagedGuiApp:
|
|||
key = msg.get("key", "")
|
||||
modifiers = msg.get("modifiers", [])
|
||||
|
||||
# Translate key name
|
||||
xkey = _KEY_MAP.get(key, key)
|
||||
# Skip release events — xdotool key handles press+release
|
||||
if action != "press":
|
||||
return
|
||||
|
||||
# Skip standalone modifier key events
|
||||
if key in ("Control", "Shift", "Alt", "Meta"):
|
||||
return
|
||||
|
||||
await self._ensure_focus(env)
|
||||
wid = str(self.window_id)
|
||||
|
||||
# Determine which modifiers are held (excluding Shift for printable chars)
|
||||
active_mods = []
|
||||
for m in modifiers:
|
||||
xmod = _KEY_MAP.get(m, m.lower())
|
||||
if xmod in ("ctrl", "alt", "super"):
|
||||
active_mods.append(xmod)
|
||||
|
||||
# Single printable character with no ctrl/alt/super modifiers → use xdotool type
|
||||
# This handles all special characters (@, #, !, etc.) correctly
|
||||
if len(key) == 1 and not active_mods:
|
||||
await self._xdotool(
|
||||
"type", "--delay", "0", "--clearmodifiers", "--window", wid, key,
|
||||
env=env,
|
||||
)
|
||||
return
|
||||
|
||||
# Non-printable key or key combo with modifiers → use xdotool key
|
||||
xkey = _KEY_MAP.get(key, key)
|
||||
if xkey in ("ctrl", "shift", "alt", "super"):
|
||||
return
|
||||
|
||||
# Build modifier prefix
|
||||
# For modifier combos, include shift too
|
||||
mod_parts = []
|
||||
for m in modifiers:
|
||||
xmod = _KEY_MAP.get(m, m.lower())
|
||||
if xmod not in ("ctrl", "shift", "alt", "super"):
|
||||
continue
|
||||
if xmod in ("ctrl", "shift", "alt", "super"):
|
||||
mod_parts.append(xmod)
|
||||
|
||||
if mod_parts:
|
||||
xkey = "+".join(mod_parts) + "+" + xkey
|
||||
|
||||
wid = str(self.window_id)
|
||||
if action == "press":
|
||||
await self._xdotool(
|
||||
"windowfocus", "--sync", wid,
|
||||
"key", "--window", wid, xkey,
|
||||
env=env,
|
||||
)
|
||||
# release events handled implicitly by xdotool key
|
||||
await self._xdotool("key", "--window", wid, xkey, env=env)
|
||||
|
||||
async def _xdotool(self, *args, env=None):
|
||||
"""Run xdotool and wait for completion."""
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
XDOTOOL_BIN, *args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
|
|
@ -488,6 +519,28 @@ class ManagedGuiApp:
|
|||
)
|
||||
await asyncio.wait_for(proc.communicate(), timeout=5)
|
||||
|
||||
async def _xdotool_fire(self, *args, env=None):
|
||||
"""Run xdotool without waiting — fire and forget for low-latency ops."""
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
XDOTOOL_BIN, *args,
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
env=env,
|
||||
)
|
||||
# Don't await — let it complete in background
|
||||
asyncio.create_task(self._reap(proc))
|
||||
|
||||
@staticmethod
|
||||
async def _reap(proc):
|
||||
"""Reap a fire-and-forget subprocess to avoid zombies."""
|
||||
try:
|
||||
await asyncio.wait_for(proc.wait(), timeout=5)
|
||||
except Exception:
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ---- Lifecycle ----
|
||||
|
||||
def kill(self):
|
||||
|
|
|
|||
Loading…
Reference in a new issue