Skip to main content

Goals, in priority order

1

Lowest possible input lag

The headline feature. Every design choice defers to it.
2

Cross-platform

macOS ⇄ Windows (Linux best-effort).
3

Secure by default

Authenticated encryption on every channel.
4

Open source & self-hostable

No accounts, no cloud, LAN-only.
5

Feature-complete vs. paid tools

Clipboard + file transfer + seamless edge switching.

The big picture

        SERVER (has the physical keyboard & mouse)      CLIENT (receives input)
   ┌──────────────────────────────────────┐        ┌──────────────────────────────┐
   │ capture (rdev grab)                    │        │ emit (enigo)                 │
   │   captures + can SWALLOW local input   │        │   injects received events    │
   │        │                               │        │        ▲                     │
   │        ▼  coalesced per tick           │  UDP   │        │                     │
   │   InputChannel ───────────────────────────────► │   InputChannel              │
   │   (encrypted, seq-numbered)            │ input  │   (decrypts, applies)        │
   │                                        │        │   cursor.rs tracks position  │
   │   Control (active? entry edge)         │◄────── │   → sends Leave to return    │
   ├────────────────────────────────────────┤  TCP   ├──────────────────────────────┤
   │ clipboard.rs (arboard) ─┐              │  bulk  │  ┌─ clipboard.rs (arboard)   │
   │ filexfer.rs (chunks)  ──┼─ BulkConn ───────────────┼─ filexfer.rs (received/)  │
   │                         └ (encrypted)  │        │  └                           │
   └────────────────────────────────────────┘        └──────────────────────────────┘
                 ▲ mDNS advertise                             ▲ mDNS discover

Why two channels?

Input and bulk data have opposite requirements:
Input (mouse/keys)Bulk (clipboard/files)
Needslowest latencyreliable, ordered delivery
Tolerates loss?yes (next move supersedes)no
TransportUDPTCP
Whyno head-of-line blockinga lost byte corrupts the paste
Mixing them would force the fast path to wait for retransmits of the slow path. Keeping them separate is the single most important latency decision — see Decision #1.

Crate layout

crates/
  protocol/   # shared, dependency-light: wire types + crypto. No OS calls.
  app/        # the binary: transport, capture/emit, features, CLI, tray.
protocol is deliberately tiny and platform-agnostic so it can be unit-tested anywhere and reused (e.g. a future mobile client). app holds everything that touches the OS or the network.

Module map (crates/app/src)

ModuleResponsibilityNative?
main.rsCLI parsing (clap); dispatch; no-arg → tray
transport.rsUDP InputChannel: framing, seq numbers, encryption
bench.rsLoopback latency benchmark (the primary metric)
bulk.rsTCP BulkConn: length-prefixed frames, handshake, encryption
filexfer.rsChunked file send + reassembly (received/)
config.rsTOML settings + monitor manager (machine/edge layout)
edge.rsServer-side screen-edge hit detection
cursor.rsClient-side cursor integration for auto-return
control.rsShared control state (active? entry edge)
capture.rsrdev global grab: capture + suppress local inputYes
emit.rsenigo input injectionYes
keymap.rsrdev ⇄ portable Key ⇄ enigo translationYes
clipboard.rsBidirectional clipboard sync (text + image)Yes
discovery.rsmDNS advertise/browseYes
run.rsWires everything into serve / connect loopsYes
tray.rsMenu-bar / system-tray GUI (tray feature)Yes, plus tray
Non-native modules build and test with --no-default-features, which is what CI uses for fast, permissionless unit tests.

Feature flags

FlagPulls inPurpose
native (default)enigo, rdev, arboard, mdns-sdreal input/clipboard/discovery
traynative + tray-icon, taothe GUI menu-bar/tray app
The layering keeps the testable core (transport, protocol, crypto, config, edge, cursor, file framing) free of any display or OS-permission dependency — that’s why 23 unit tests run headlessly in CI.

How a keypress reaches the other machine

1

Capture & swallow

capture.rs (rdev grab) sees the key. If control is on the client, it maps the native key to a portable Key, forwards it, and swallows it locally.
2

Coalesce & seal

run.rs::run_server_input coalesces the tick’s events into one InputMsg::Events, and transport.rs seals + sends it over UDP.
3

Decrypt & de-dup

On the client, transport.rs decrypts, drops stragglers by sequence number, and hands the batch to run.rs::connect.
4

Inject & track

emit.rs injects each event with enigo. cursor.rs integrates mouse deltas; if the cursor crosses back over the border edge it sends InputMsg::Leave.

Control handoff (the “KVM” part)

Two ways control moves from server to client:
  • Automatic edge switch: edge.rs detects the server cursor hitting a bordered screen edge (from the monitor-manager layout) → Control.active = true and the entry position is recorded.
  • F12 hotkey: manual toggle, always available as a fallback.
Return is symmetric: the client’s cursor.rs detects the cursor crossing back and sends Leave; the server clears active. See Decision #7.

Next

Wire protocol

The exact message types on each channel.

Latency

Where the microseconds actually go.