feat: remap keys on their way to other devices - #483
Open
khac88 wants to merge 5 commits into
Open
Conversation
Adds `[input_pre_processing.remap_keys]`, rewriting individual keys just before they go on the wire. The motivating case is modifier layouts differing across operating systems: sending Command from a Mac as Control (and Control as Super) keeps Cmd+C copying on a Windows peer. Remapping is deliberately send-side only, so everything local — the release bind, the host's own shortcuts — keeps seeing the physical keys. Two things a naive remap gets wrong, both covered by tests: - a modifier arrives twice, once as a key and once as a mask update, so `KeyboardEvent::Modifiers` has to be rewritten alongside the key or the peer holds one modifier while its mask claims another. A swap needs every source bit cleared before any target bit is set. - `release_capture` synthesizes key-up events from the physical keys it recorded, so those have to go through the same remap or the peer is released from a key it was never pressed with. Related to feschber#361
Adds `[input_pre_processing.invert_scroll_vertical]` and `invert_scroll_horizontal`, flipping the sign of outgoing scroll axis events. The motivating case is macOS' "natural scrolling" reaching a Windows or Linux peer that scrolls the traditional way, making the wheel feel backwards on the remote machine. Like `remap_keys`, this happens on the sending side only, so scrolling on the local machine itself is unaffected.
Crossing an edge now carries the cursor's position along that edge over to the peer — leaving near the top of the right edge enters the peer near the top of its left edge, scaled to the peer's own display size if the two differ. Previously the peer always entered at the edge's midpoint, unrelated to where the cursor actually crossed. `ProtoEvent::Enter` gains a normalized (0.0..=1.0) cross-axis position, computed on the capturing side from the raw crossing point and applied on the receiving side as an absolute cursor warp before relative motion resumes. The same position flows the other way through `ProtoEvent::Leave` for the hand-back case (moving the cursor further past the edge it just entered at, without a matching `[[clients]]` on the other side): the side being left reports the spot its own local crossing landed on, so the cursor reappears there on the peer instead of snapping back to the original entry point. macOS implements both directions (reporting on capture, applying on emulation). Windows implements the emulation (receiving) side only — its capture side still needs the same per-display-RECT math macOS already has via a single union bounding box. X11/Wayland backends are unimplemented captures/emulations already and are unaffected either way. All of these fall back to the edge's midpoint, matching the previous behavior exactly, so nothing regresses.
Adds `[[input_pre_processing.remap_chords]]`, layered on top of `remap_keys`. A plain per-key remap can't tell "Command alone" from "Command as part of a chord" — it always sends a given key as the same thing. `remap_chords` covers that: a modifier is sent as something else specifically when a configured trigger key is pressed while it's held, without touching what it sends as the rest of the time. The motivating case is `Cmd+Tab` reaching a Windows peer as `Alt+Tab` (its app switcher) instead of `Ctrl+Tab`, while `Cmd+C` still becomes `Ctrl+C` via the existing Command→Control mapping. A chord-eligible modifier's down event is buffered instead of sent immediately, since its outcome isn't known until the next event: - the configured trigger key follows → the buffered down (and the paired `Modifiers` mask update, also held back) resolve to the override, and stay resolved for the rest of that hold - anything else follows → resolve as the plain `remap_keys` mapping instead, same as if no chord rule existed - another modifier follows (e.g. Shift, for `Cmd+Shift+Tab` cycling backwards) → left alone, so it doesn't cancel a chord in progress - plain cursor motion never resolves anything, so an incidental mouse twitch while holding the modifier can't break a chord either `KeyRemap::apply` now returns zero, one, or two events per physical event instead of exactly one, so `release_capture`'s synthesized key-up-on-disconnect path needed its own chord awareness too: a still-`pending` modifier never had a down event sent for it, and `release_key` reports that (`None`) rather than releasing a key the peer was never told was pressed.
An input method engine composing text (e.g. a Vietnamese IME like OpenKey doing Telex/VNI diacritics) deletes the real keystroke from the event stream and re-injects a synthetic correction: a Backspace plus a "letter" event carrying a placeholder keycode, with the actual composed character riding along in a Unicode-string field this capture never reads. Both apps install the same kind of system-wide CGEventTap (session-level, head-insert), and macOS doesn't give either one control over which sees a keystroke first. When the IME's tap processes it first, our tap only ever sees its placeholder-keycode correction — and forwards that to the peer as if it were what was typed, producing wrong characters on the remote machine. Genuine hardware input reports an event source state of `HIDSystemState`; synthetic events from another process's own `CGEventSourceCreate` typically don't. Drop keyboard events that don't carry it instead of translating and forwarding them — consumed locally same as a real key, just never sent to the peer. This isn't Vietnamese-specific: it's a general defense against anything that injects synthetic keystrokes system-wide while capture is active.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related to #361.
Adds
[input_pre_processing.remap_keys], rewriting individual keys just beforethey go on the wire:
The motivating case is the one from #361 — modifier layouts differing across
operating systems. Sending Command from a Mac as Control (and Control as Super)
keeps
Cmd+Ccopying on a Windows or Linux peer instead of firing whateverSuper+C happens to be bound to.
Approach
Remapping is deliberately send-side only, applied right before events go on
the wire. Everything local — the release bind, enter binds, the host's own
shortcuts — keeps seeing the physical keys, so the remap cannot lock the user
out of their own machine.
The section name follows the direction set in #347:
input_post_processingfortransformations on events received from other devices,
input_pre_processingfor events sent to them. If #471 lands first I am happy to rebase onto
whatever shape it settles on.
Two things a naive remap gets wrong
Both are covered by tests:
Rewriting only the key leaves the peer holding one modifier while its
KeyboardEvent::Modifiersmask claims another. A swap also needs every sourcebit cleared before any target bit is set, or
A→Btogether withB→Adropsone of the two.
release_capturesynthesizes key-up events from the physical keys itrecorded, so those have to go through the same remap — otherwise the peer is
released from a key it was never pressed with and keeps holding the one it
actually got.
Testing
cargo fmt --check,cargo clippy --workspace --all-targets --all-features -D warningsand
cargo test --workspace --all-featuresclean on macOS;cargo check/cargo clippyclean forx86_64-pc-windows-msvc.Cmd+CandCmd+Xarrive asCtrl+C/Ctrl+Xon the Windows peer.Not in this PR
Per-client remaps. #361 asks for it per client/server pair; this is global
because that is the smaller change and matches the section being about the
sending side as a whole. Happy to extend it if you would rather have it keyed
per client.