- Reduce cooldowns from 1000ms to 300ms (spurious presses happen within ~100-400ms)
- ReceivedControl cooldown now only blocks the 'from' direction, not all directions
- Return cooldown blocks specific direction and clears after blocking once
- Prevents bounce-back at edge while allowing normal navigation
When RECOVERY does movefocus (because we're not at edge), Hyprland fires
an IPC Move callback. If movefocus moved us to the edge window, IPC Move
would see at_edge=true and initiate a transfer, causing a single
Super+Arrow to both move focus AND transfer.
Set last_control_return after RECOVERY movefocus so the IPC Move callback
hits the cooldown and skips transfer initiation.
When Super+Arrow triggers a transfer, the keypress gets forwarded to the
receiving machine and causes Hyprland to fire IPC Move. This resulted in
focus moving an extra window after the transfer.
Add a 1-second cooldown after entering ReceivedControl during which all
IPC Move events are ignored.
When RECOVERY hotkey and Hyprland IPC Move both fire for the same
Super+Arrow keypress, the IPC Move handler would do movefocus even
though a transfer was already being initiated. This caused double
navigation (cursor would move two windows instead of one).
Now IPC Move skips entirely if state is Initiating in the same direction.
When recovery mode detects a hotkey (Super+Arrow) it breaks out of the
event loop immediately. If Super was released right after the arrow key,
that release event would be pending but unconsumed, causing super_held
to remain true and incorrectly entering PostRecovery with a stuck Super.
Now we query the physical key state via get_key_state() before deciding
whether to enter PostRecovery, ensuring we don't create a synthetic
Super key-down when the key was already released.
RECOVERY mode used monitor-proximity detection for Up/Down while
IPC Move used window-based detection, causing inconsistent behavior.
Now both use the same window-based detection for all directions.
The edge detection was triggering immediately after entering ReceivedControl
because Hyprland's cursor position query returned stale data before the warp
took effect. Add 500ms cooldown after entering the state.
When receiving control, the cursor was positioned at the exact screen
edge (e.g., y=1439 on a 1440-height screen). This caused immediate
re-triggering of edge detection, sending control right back.
Add 30-pixel inset from edge when positioning cursor on entry.
Machines without physical input devices (like hatake) can still:
- Receive control (ReceivedControl state)
- Relay input to other machines (Relaying state)
- Return control to the source
But they cannot initiate transfers from Local state since there's
nothing to grab. This prevents the deadlock where hatake would
enter RemoteActive and call StartCapture, but with no devices,
no input would be forwarded.
The previous code only checked if the current window was near the
monitor edge, not if there were other windows in that direction.
This caused Super+Up to always transfer when on the edge monitor,
even if there were windows above the current one.
Now all four directions use the same logic: check if any other window
on the same monitor is further in the requested direction.
When a machine without physical input devices (e.g., hatake) is in
ReceivedControl and initiates a transfer to another direction, it now
enters Relaying mode instead of trying to grab local devices.
In Relaying mode, the machine forwards InputEvents from the original
controller to the new target, acting as a transparent relay. This
enables full multi-machine KVM chains where only one machine has
physical input devices.
New states and events:
- TransferState::Relaying { from, to } - receiving from one, forwarding to another
- TransferEvent::StartRelay { from, to } - begin relay mode
- TransferEvent::StopRelay - end relay mode
When the relay target returns control (Leave), the relay machine
resumes ReceivedControl from the original source.
Add has_devices flag to EvdevGrabber that tracks whether the machine
has physical input devices. Machines without devices (e.g. headless
servers or remote desktops) can still receive control transfers but
cannot initiate them, since they have no input to capture and forward.
This fixes a deadlock where a deviceless machine would try to initiate
a transfer, enter RemoteActive state with no evdev thread to capture
input, while the peer would wait forever in ReceivedControl for input
that never arrives.
Bars and panels at the top/bottom of monitors were causing false
positives in window-based edge detection. Now for Up/Down directions,
we check if the active window is within 100px of the monitor edge
instead of looking for windows above/below.
Monitor positions are in logical coordinates but width/height from
Hyprland are physical. Fixed edge detection to convert physical
dimensions to logical (dividing by scale) for consistent comparisons.
In multi-monitor setups with different height monitors, cursor edge
detection for Up/Down now checks if cursor is at the edge of the
specific monitor it's on, rather than using global screen bounds.
This fixes mouse-based edge detection on monitors that aren't the
tallest (e.g., cursor at y=1440 on a 1440p monitor now correctly
triggers Down edge, even if another monitor extends to y=1728).
For horizontal multi-monitor setups with different height monitors,
create barriers at the bottom of each monitor, not just the tallest.
A monitor gets an Up/Down barrier if no other monitor overlaps its
x range above/below it.
- Increase cooldown from 500ms to 1000ms
- Set cooldown after barrier-triggered returns
- Set cooldown after cursor-edge returns
- Set cooldown after keyboard-initiated returns
- Changed find_edge_output() to find_edge_outputs() returning Vec
- For Left/Right: still returns single monitor at edge
- For Up/Down: returns ALL monitors at top/bottom edge
- create_barriers() now loops through all edge outputs
- Creates barrier on each monitor for Up/Down directions
- Enables proper Up/Down neighbor configs with horizontal multi-monitor setups
When the user changes a neighbor's direction in the GUI:
1. Local config is saved with the new direction
2. DirectionChange message is sent to the affected peer
3. Peer updates its config to store us in the opposite direction
4. Both machines restart with synchronized edge barriers
This replaces the previous auto-correction behavior that would
overwrite config changes on reconnect. Now direction changes are
explicit and initiated by the user.
Changes:
- Add DirectionChange/DirectionChangeAck protocol messages
- Handle incoming DirectionChange by updating config and restarting
- Send DirectionChange to peers when IPC Reload detects direction changes
- Remove auto-correction on Hello that was overwriting GUI changes
- Use setsid --fork to properly daemonize spawned process
- Verify daemon started by checking socket availability
- Increase wait times for more reliable restart
- Add logging for exe path during restart