Minimal software KVM for sharing a Mac's keyboard and mouse with a Windows PC over LAN. Press a hotkey to send input to the PC, press again to reclaim it. Built for low-latency use. Raw input over unencrypted UDP, no video or screen sharing.
Mac (sender) UDP / LAN Windows (receiver)
CGEventTap ───────────────> 3–5 byte datagrams ────> SendInput
<──── audio (PCM over UDP) <──── WASAPI loopback
The sender captures keyboard and mouse events on macOS via CGEventTap, serializes them into UDP packets, and sends them to the receiver. The receiver injects them as native Windows input via SendInput with hardware scancodes.
Audio flows in the reverse direction: the receiver captures Windows system audio via WASAPI loopback and streams raw PCM over UDP back to the sender, which plays it through the Mac's default output device.
The preferred macOS install is Hotswitch.app.
- Drag
Hotswitch.appinto/Applications. - Launch it once.
- If no receiver address is saved yet, the app prompts for the Windows receiver address in
IP:portform. - Use the tray item
Receiver Address...any time you want to change it later.
Start on Login launches the app bundle directly and uses the saved receiver address. No command-line arguments are required once the address has been saved.
The macOS release archive also includes the bare hotswitch-sender binary for development/debugging, but the app bundle is the normal install path.
Check for Updates on macOS now stages and swaps the whole Hotswitch.app bundle, rather than patching the inner executable in place.
Download the Windows release zip. It contains:
hotswitch-receiver.exehotswitch-receiver-service.exeinstall-hotswitch.ps1start-hotswitch.ps1uninstall-hotswitch.ps1
Install or migrate from an elevated PowerShell prompt:
.\install-hotswitch.ps1The installer copies the files into C:\Program Files\Hotswitch, removes the old scheduled-task startup entry, creates or updates the Hotswitch Windows service, adds the required Private-network inbound firewall rule for UDP 24801, and starts the service.
The service launches hotswitch-receiver.exe into the active console session, where it owns the tray icon, audio loopback, and input injection.
For development, you can still run the receiver directly:
hotswitch-receiver.exe 0.0.0.0:24801
Ctrl+Escape: toggle capture- Both sides run as tray apps with status icons and menus
- Status line
- Check for Updates
- Receiver Address...
- Show Log
- Start on Login
- Quit
The sender runs optional executable hook scripts from:
~/Library/Application Support/Hotswitch/hooks/
on-receiver-connected: runs when the sender first sees the receiver.on-receiver-disconnected: runs when the receiver disappears, the sender quits, or the sender relaunches for an update/address change after having connected.
Hooks run asynchronously during normal connection changes and are logged in ~/Library/Logs/hotswitch-sender.log. Quit and relaunch cleanup waits for the disconnect hook to finish.
- Status line
- Check for Updates
- Show Log
- Start on Login
- Quit
On Windows, Start on Login controls the Hotswitch service startup type. Quit stops the current service-backed receiver session. To bring it back in the same session, run start-hotswitch.ps1 from C:\Program Files\Hotswitch or Start-Service Hotswitch from an elevated shell.
Requires Rust.
# Sender (on macOS)
cargo build --release -p hotswitch-sender
./scripts/build-sender-app.sh target/release/hotswitch-sender "$(sed -n 's/^version = \"\\(.*\\)\"/\\1/p' sender/Cargo.toml | head -1)"
cp -R target/macos-app/Hotswitch.app /Applications/
# Receiver (on Windows)
cargo build --release -p hotswitch-receiver
cargo build --release -p hotswitch-receiver-service
# Protocol tests (any platform)
cargo test -p hotswitch-protoEvery push to main triggers a GitHub Actions build that publishes:
hotswitch-sender-aarch64-apple-darwin.tar.gzhotswitch-receiver-x86_64-pc-windows-msvc.zip
The macOS tarball contains Hotswitch.app and the bare hotswitch-sender binary. The app bundle keeps the sender as a menu bar app (LSUIElement) while adding app-bundle metadata for Game Mode eligibility and normal macOS install behavior. The Windows zip contains the receiver, the receiver service, and the install/uninstall scripts. The version is derived from the commit timestamp.
Plain UDP, 1-byte type tag, big-endian, no framing:
| Event | Port | Bytes | Layout |
|---|---|---|---|
| Mouse move | 24801 | 5 | 01 dx:i16 dy:i16 |
| Mouse button | 24801 | 3 | 02 button:u8 pressed:u8 |
| Scroll | 24801 | 5 | 03 dx:i16 dy:i16 |
| Key event | 24801 | 4 | 04 cgkeycode:u16 pressed:u8 |
| Key sync | 24801 | 2+2n | 05 count:u8 [cgkeycode:u16]... |
| Heartbeat | 24801 | 1 | 06 |
| Audio | 24802 | 3+4n | 07 channels:u16 [sample:f32le]... |
hotswitch/
proto/ Shared protocol library
sender/ macOS sender (CGEventTap + UDP + tray icon)
receiver/ Windows session receiver (UDP + SendInput + tray icon)
receiver-service/ Windows service that launches the receiver into the active session
scripts/ Build and install helpers
Huge thanks to Moonlight/Sunshine and LAN Mouse. This project wouldn't exist without their implementations, which were invaluable references for the input and audio handling. Audio streaming was also informed by Beer and W11-to-Mac-Sound-Stream.