mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 10:00:34 +02:00
Pad: keep typing in order and acknowledge it once, with accents through one KDE Connect queue
Review found accent-only input repeated forever (sent was false), text split across two transports out of order, accents stuck until the next keypress, duplicated on retry, and racy. A batch with an accent now sends its whole keyboard through KDE Connect; later keys follow while any are queued; the server owns one ordered, bounded queue drained by a single thread; sent means the whole batch is taken. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dc7854d10b
commit
9f3fb89e30
2 files changed
+157
-28
No files matched your search
+62
-23
@@ -856,14 +856,23 @@ PAD_CLICKS = {"singleclick": "left", "rightclick": "right", "middleclick": "midd
|
||||
PAD_NOTCH = 15 # scroll units for one wheel notch
|
||||
|
||||
|
||||
def pad_events(events):
|
||||
"""The trackpad's and key row's events as (gamescope events, text for KDE Connect).
|
||||
def typed_in_kde(events):
|
||||
"""Whether these events must go the KDE Connect way: they hold an accent or emoji, or
|
||||
earlier ones are still waiting there (typing must stay in order, so it can't be split)."""
|
||||
with _kde_lock:
|
||||
waiting = bool(_kde_queue)
|
||||
return waiting or any(ch not in frame_touch.ASCII for e in events for ch in e.get("key", ""))
|
||||
|
||||
|
||||
def pad_events(events, kde=False):
|
||||
"""The trackpad's and key row's events as (gamescope events, KDE Connect events).
|
||||
|
||||
Gamescope's own input reaches every panel and needs nothing installed, but types only
|
||||
US-keyboard characters; accents and emoji go the KDE Connect way.
|
||||
US-keyboard characters. With kde, the keyboard goes to KDE Connect as it was sent, so
|
||||
text stays in order; the pointer always goes through gamescope.
|
||||
"""
|
||||
ascii_keys = frame_touch.ASCII
|
||||
out, extra = [], []
|
||||
out, slow = [], []
|
||||
|
||||
def tap(code, mods):
|
||||
down = [{"key": c, "down": True} for c in mods]
|
||||
@@ -885,12 +894,15 @@ def pad_events(events):
|
||||
out.append({"button": "left", "down": True})
|
||||
if e.get("singlerelease"):
|
||||
out.append({"button": "left", "down": False})
|
||||
if kde and ("key" in e or "specialKey" in e):
|
||||
slow.append({k: e[k] for k in ("key", "specialKey", "ctrl", "alt", "shift", "super") if k in e})
|
||||
continue
|
||||
if "specialKey" in e and e["specialKey"] in PAD_KEYS:
|
||||
tap(PAD_KEYS[e["specialKey"]], mods)
|
||||
if "key" in e:
|
||||
for ch in e["key"]:
|
||||
if ch not in ascii_keys:
|
||||
extra.append(ch)
|
||||
continue # only reached when kde is wrong; the caller checks typed_in_kde first
|
||||
elif mods:
|
||||
code, shifted = ascii_keys[ch]
|
||||
tap(code, mods + ([42] if shifted and 42 not in mods else []))
|
||||
@@ -899,36 +911,63 @@ def pad_events(events):
|
||||
out[-1]["text"] += ch
|
||||
else:
|
||||
out.append({"text": ch})
|
||||
return out, "".join(extra)
|
||||
return out, slow
|
||||
|
||||
|
||||
def remote_input(body):
|
||||
"""{"events": [...]} sends keyboard and pointer events; {} (or none yet) just starts the agent."""
|
||||
"""{"events": [...]} sends keyboard and pointer events; {} (or none yet) just starts the agent.
|
||||
|
||||
"sent" in the answer means the whole batch is taken (the page keeps it and tries again
|
||||
if not), so the keyboard's share is queued only once that's so.
|
||||
"""
|
||||
events = body.get("events", [])
|
||||
if not isinstance(events, list) or len(events) > INPUT_BATCH_LIMIT:
|
||||
raise Failure(f"events must be a list of at most {INPUT_BATCH_LIMIT}", 400)
|
||||
events = [input_event(e) for e in events]
|
||||
touch, accents = pad_events(events)
|
||||
touch, slow = pad_events(events, typed_in_kde(events))
|
||||
status = _touch.send(touch)
|
||||
if accents:
|
||||
_accents.append(accents)
|
||||
_send_accents(status.get("sent") or not touch)
|
||||
return status
|
||||
if touch and not status.get("sent"):
|
||||
return status
|
||||
if slow:
|
||||
_queue_for_kde(slow)
|
||||
return {**status, "sent": True}
|
||||
|
||||
|
||||
_accents = [] # accents and emoji waiting for KDE Connect, which starts the first time they're typed
|
||||
# Typing KDE Connect is to do: it starts the first time it's needed, so this waits for it.
|
||||
_kde_queue = []
|
||||
_kde_lock = threading.Lock()
|
||||
_kde_worker = [None]
|
||||
KDE_QUEUE_LIMIT = 200
|
||||
KDE_WAIT = 90 # seconds to wait for KDE Connect before giving up on what's queued
|
||||
|
||||
|
||||
def _send_accents(wanted):
|
||||
"""Type what gamescope can't through KDE Connect, starting it if needed; kept until it's up."""
|
||||
if not _accents or not wanted:
|
||||
return
|
||||
status = _input.send([{"key": "".join(_accents)}])
|
||||
if status.get("sent"):
|
||||
_accents.clear()
|
||||
elif status.get("state") == "error":
|
||||
_accents.clear() # not coming; don't hold them forever
|
||||
del _accents[:-50]
|
||||
def _queue_for_kde(events):
|
||||
with _kde_lock:
|
||||
_kde_queue.extend(events)
|
||||
del _kde_queue[:-KDE_QUEUE_LIMIT]
|
||||
if not (_kde_worker[0] and _kde_worker[0].is_alive()):
|
||||
_kde_worker[0] = threading.Thread(target=_drain_kde, daemon=True)
|
||||
_kde_worker[0].start()
|
||||
|
||||
|
||||
def _drain_kde():
|
||||
"""The only sender: in order, dropping from the queue just what went, until it's empty."""
|
||||
deadline = time.time() + KDE_WAIT
|
||||
while True:
|
||||
with _kde_lock:
|
||||
batch = _kde_queue[:INPUT_BATCH_LIMIT]
|
||||
if not batch:
|
||||
return
|
||||
status = _input.send(batch)
|
||||
with _kde_lock:
|
||||
if status.get("sent"):
|
||||
del _kde_queue[:len(batch)]
|
||||
deadline = time.time() + KDE_WAIT
|
||||
elif status.get("state") == "error" or time.time() > deadline:
|
||||
_kde_queue.clear() # not coming; don't hold it forever
|
||||
return
|
||||
if not status.get("sent"):
|
||||
time.sleep(0.4)
|
||||
|
||||
|
||||
# ---- touch: the headset's panels, through gamescope's own input (frame_touch.py) ----
|
||||
|
||||
Reference in new issue
Block a user