Recovery mode improvements:
- Remove arbitrary timeout, exit when Super key is released instead
- Add same at_edge check as IPC Move before initiating transfer
- When not at edge, do movefocus ourselves (libinput dropped the keypress)
The key insight is that libinput's stale state causes it to DROP the
keypress entirely, so we must handle it ourselves in all cases - either
initiating transfer (at edge) or doing movefocus (not at edge).
After releasing the evdev grab, libinput has stale keyboard state - it
thinks the arrow key used to initiate the original transfer is still
pressed because it never saw the release event (it happened during the
grab).
New approach: Use evdev's uinput module to create a temporary virtual
keyboard device and send key-up events for all arrow keys. This gives
libinput fresh key-up events from a real evdev device, which should
clear the stale state.
Also removed the ineffective "recovery mode" that was trying to detect
keybinds directly but couldn't because the triggering keypress happened
before we entered recovery mode.
After control returns to the local machine, libinput has stale keyboard
state from before the evdev grab (thinks keys are still pressed when
they're not). This causes the first keypress to be eaten.
Solution: Instead of immediately releasing the evdev grab, enter a
"recovery mode" that:
1. Keeps the evdev grab active for up to 500ms
2. Queries physical key state to detect if Super is already held
3. Monitors evdev events for the Super+Arrow keybind pattern
4. If detected, triggers the transfer ourselves via hyprkvm move
5. If timeout expires without keybind, releases the grab normally
This bypasses libinput's stale state entirely by reading input directly
from evdev and triggering the transfer via IPC.
Also cleaned up evdev_grab.rs by removing non-functional send_events()
calls (you can't write to physical evdev devices from userspace).
Previous fix blindly sent key-UPs for all modifiers, which broke the
case where user is still holding Super - they'd have to release and
re-press it.
Now we query the actual physical key state after ungrab:
- Arrow keys: if NOT pressed, send UP (clear stale state)
- Modifiers: if STILL pressed, send UP+DOWN (give fresh edge)
This ensures:
1. Stale arrow key state is cleared
2. Held modifiers get a fresh edge so keybindings fire
3. Released modifiers stay released
The virtual keyboard key-ups weren't working because Hyprland tracks
physical and virtual keyboards separately. The physical keyboard state
was stale (Super+Right still pressed from before the grab).
Now we synthesize key-up events directly on the evdev device AFTER
ungrabbing. These events go through libinput and update Hyprland's
physical keyboard state, so subsequent presses are fresh edges that
trigger keybindings.
The key insight: while evdev grab is active, Hyprland only sees our
virtual keyboard. If we inject key-ups while the grab is active,
Hyprland's keyboard state becomes "no keys pressed". Then when we
release the grab and Hyprland starts seeing the physical keyboard
again, new keypresses are fresh edges that trigger keybindings.
Previously we were injecting key-ups AFTER releasing the grab. By that
point Hyprland already saw the physical keyboard with stale state
(Super still pressed from before the grab started), so our virtual
key-ups didn't help - the physical state took precedence.
The previous StopCapture fix only sent key() events for modifiers but
didn't send the modifiers(0,0,0,0) event that explicitly clears the
modifier mask. The injection side's reset_all_keys() does both, plus
flushes the connection.
Now StopCapture uses reset_all_keys() to match the injection side fix.
The previous fix addressed the injection side (remote machine). This
completes the fix for the capture side (local machine initiating transfer).
When a transfer is initiated via keybinding (e.g., Super+Right), Hyprland
sees the modifier and arrow key DOWN events, but then evdev grabs the
input before Hyprland sees the corresponding UP events. After the grab
releases, Hyprland still thinks those keys are pressed.
Now StopCapture injects key-up events for both the arrow key and all
common modifiers (Meta, Shift, Ctrl, Alt) to reset Hyprland's state.
The previous fix only released modifiers. The actual issue was that
the arrow key that triggers the return (e.g., LEFT) is injected DOWN
but never UP - the StopInjection happens immediately after the
keybinding fires, before the key-up event arrives.
On the next transfer, Hyprland still thinks that arrow key is pressed,
so the first press is seen as a repeat and the keybinding doesn't fire.
Fix: Track all pressed keys in a HashSet and release them all when
stopping injection, not just modifiers.
Root cause: When injection stops, the ModifierTracker still has
modifiers marked as pressed (e.g., Super from the synthetic keydown).
When the next transfer starts, we send another Super DOWN, but
Hyprland sees this as a repeat since Super was never released.
The first keypress isn't recognized as a fresh key combo.
Fix: Add reset_modifiers() to VirtualKeyboard that releases all
pressed modifiers and resets internal state. Call it in StopInjection
so each injection session starts with clean modifier state.
- Add RAW EVDEV logging to see kernel-level key events
- Drain pending events before grabbing to start fresh
- Drain stale events from channel after stopping grab
When a transfer is initiated via Super+Arrow, the Super key is already
held before the evdev grab starts. This means the grabber never sees
the initial Super key-down event. Send a synthetic Super key-down as
the first event when starting capture so the destination machine knows
Super is pressed.
- Add DEBUG logging for key capture (CAPTURE KeyDown/KeyUp)
- Add DEBUG logging for key injection (INJECT KEY with modifier state)
- Remove synthetic modifier key-ups that corrupted compositor state
- Inject arrow key-up for the direction used to initiate transfer
when stopping capture (attempt to fix stuck key state)
Keyboard return debugging still in progress - first keypress in
return direction is still being eaten after control transfer.
- Add MonitorInfo struct to pass Hyprland monitor positions to edge capture
- Match Wayland outputs to Hyprland monitors by size, considering scaling
(handles 1.5x and 2.0x scale factors for monitors like 4K at 1.5x)
- Add multiple roundtrips during initialization to ensure output info
- Add 500ms cooldown after receiving Leave to prevent bounce-back loops
where control immediately transfers back after returning
- Add barrier creation logging for debugging
Keyboard return debugging still in progress (first keypress issue).
When in ReceivedControl state, check for return-to-source BEFORE
doing the at_edge check. The edge detection can fail due to
hyprland query timing, causing the first keypress to fall through
to local movefocus instead of returning control.
Now: ReceivedControl + direction matches source = always return control
When control returns from remote machine, the local compositor never
saw key-up events for modifiers that were released while input was
grabbed. This caused "stuck" modifier state where the compositor
thought SUPER was still pressed, preventing keybinds from working.
Fix: send synthetic key-up events for all modifier keys (Shift, Ctrl,
Alt, Super) via virtual keyboard after stopping the input grab.
Also adds debug logging to IPC server for troubleshooting.
Only return control when BOTH conditions are met:
1. At edge (edge monitor + edge window)
2. In ReceivedControl state from that direction
Previously, ReceivedControl check happened before edge detection,
causing every move in the "from" direction to return control.
When in ReceivedControl state and user presses the key to move in the
direction control came from, return control to source machine instead
of doing local movefocus or edge detection.
This enables returning control via keyboard (SUPER+Arrow) from the
receiving machine back to the source.
1. Edge detection now checks monitor AND window position:
- On edge monitor (no monitor in that direction)
- On edge window (no window further in that direction)
2. Fixed bug where movefocus was not called when at edge but no peer
connected. Now movefocus is always executed unless we're transferring.
3. Only initiate transfer when BOTH at edge AND have connected peer.
Add IPC mechanism between CLI and daemon so `hyprkvm move <direction>`
can trigger machine transfers when at screen edges.
- Add Unix socket IPC server at $XDG_RUNTIME_DIR/hyprkvm.sock
- Daemon handles Move requests by checking cursor position
- If cursor at edge with connected peer, initiates transfer
- Otherwise returns DoLocalMove for normal hyprctl movefocus
- CLI gracefully falls back to local move if daemon not running
This enables the key HyprKVM feature: using SUPER+Arrow keys to
seamlessly switch between machines when at workspace boundaries.
When pressing modifier keys (Shift, Ctrl, Alt, Super), the key event
was sent but the XKB modifier state was never updated. This caused
the compositor to not recognize modifiers as held.
Added ModifierTracker to track pressed modifier keys and send
keyboard.modifiers() calls with the appropriate XKB modifier masks
after each modifier key press/release.
- Evdev grabber: accumulate motion deltas before sending
- Main loop: coalesce motion events from queue into single message
- Combines REL_X/REL_Y into single motion event
- Dramatically reduces message count during fast mouse movement