-
-
Notifications
You must be signed in to change notification settings - Fork 77
Input System
The input system allows a phone or tablet to function as a wireless trackpad and keyboard for the host computer. It converts browser touch gestures into native operating system input events while maintaining low latency and platform-independent behavior.
From a user's perspective, every interaction feels like using a physical trackpad. Internally, Rein recognizes gestures, transports them over WebRTC DataChannels, validates incoming messages, and injects native input events using the appropriate operating system APIs.
Every input event follows the same processing pipeline.
Gesture recognition is handled by useTrackpadGesture (src/hooks/useTrackpadGesture.ts).
The hook listens for browser touch events (touchstart, touchmove, touchend, and touchcancel) and converts them into higher-level input actions that can be transmitted to the host.
Internally it maintains a map of active touch points and continuously updates gesture state as fingers are added, moved, or removed.
| Fingers | Gesture | Generated Event |
|---|---|---|
| 1 | Move | move |
| 1 | Tap | Left click |
| 2 | Tap | Right click |
| 3 | Tap | Middle click |
| 1 | Long press + move | Drag |
| 2 | Pan | scroll |
| 2 | Pinch | zoom |
| Scroll mode | One finger | Scroll |
When one-finger scroll mode is enabled, Rein prevents accidental diagonal scrolling by locking movement to the dominant axis.
if (absDx > absDy * axisThreshold)
dy = 0
else if (absDy > absDx * axisThreshold)
dx = 0The default threshold is 2.5, meaning one direction must be significantly stronger before the other axis is ignored.
Small unintended movements are ignored before a gesture begins.
| Fingers | Threshold |
|---|---|
| 1 | 10 px |
| 2 | 15 px |
| 3+ | 15 px |
Dragging is implemented by delaying the release of an initial tap.
Once a gesture has been recognized, it is serialized into an InputMessage and sent over a WebRTC DataChannel.
Rein uses two separate DataChannels to balance responsiveness and reliability:
-
input-unorderedchannel — mouse movement, scrolling, zooming, and touch events (maxRetransmits=0). -
input-orderedchannel — clicks, keyboard events, text input, key combinations, copy/paste, and ping/pong heartbeats (ordered=true).
Continuous movement generates hundreds of updates per second. Retransmitting lost packets would only increase latency because the latest cursor position is always more valuable than an older one. Keyboard events, on the other hand, must never be lost or reordered.
ConnectionProvider automatically routes each message to the appropriate channel and falls back to the other channel if necessary.
const isUnordered =
type === "move" ||
type === "scroll" ||
type === "touch" ||
type === "zoom"To estimate connection latency, ConnectionProvider periodically exchanges ping and pong messages over the reliable DataChannel:
Client (ConnectionProvider)
│ { type: "ping", timestamp: Date.now() }
▼
Server (WebRTCManager)
│ { type: "pong", timestamp: timestamp }
▼
Client
The client measures RTT (Date.now() - timestamp) every 2 seconds. RTT telemetry is reported every 10 seconds to POST /api/debug/report-latency, allowing the host Debug Dashboard (/debug) to display real-time latency charts.
InputHandler (src/server/InputHandler.ts) is responsible for processing every incoming input message on the host.
Before an event reaches the operating system it passes through three stages:
- Validation and sanitization
- Movement acceleration and rate limiting (8 ms throttle)
- Platform driver dispatch
Incoming messages are sanitized before processing:
- truncating excessively long text
- clamping movement coordinates
- validating button names
- validating modifier combinations
These checks prevent malformed or malicious messages from reaching the platform injectors.
Mouse movement and scrolling can generate thousands of events per second.
To avoid overwhelming the operating system, Rein applies an 8 ms leading-edge throttle with a trailing flush.
This means:
- the first event is processed immediately,
- intermediate events are merged into aggregate delta offsets,
- the final accumulated movement is always delivered.
The result is smooth 125 Hz cursor movement while avoiding CPU bottlenecking.
Rein applies an acceleration curve similar to a native desktop touchpad:
Small movement ──► Precise cursor movement
Large movement ──► Accelerated cursor movement
The acceleration curve is controlled by ACCEL_THRESHOLD, ACCEL_FACTOR, ACCEL_EXPONENT, and user-configurable sensitivity.
After processing, each message is forwarded to the platform injector.
| Message | Native Action |
|---|---|
move |
Move mouse |
click |
Mouse button |
scroll |
Mouse wheel |
zoom |
Ctrl + Mouse Wheel |
key |
Single key |
combo |
Keyboard shortcut |
text |
Text input |
touch |
Native touch contacts |
copy |
Copy shortcut |
paste |
Paste shortcut |
The final stage converts processed input into native operating system events:
| Platform | Implementation | Mechanism |
|---|---|---|
| Linux | LinuxInputInjector |
/dev/uinput virtual devices created via Koffi C FFI |
| macOS | MacInputInjector |
CoreGraphics (CGEvent) system event injection |
| Windows | WindowsInputInjector |
Win32 SendInput and Synthetic Pointer API |
The correct injector is loaded automatically during startup based on os.platform().
The input system can be reconfigured without restarting the application. When client devices change settings via POST /api/config, WebRTCManager.updateConfig() applies settings directly to all active InputHandler instances:
| Setting | Purpose |
|---|---|
sensitivity |
Mouse speed multiplier |
invertScroll |
Reverse scroll direction |
inputThrottleMs |
Minimum interval between mouse updates (default 8ms) |
Since touchscreens do not have physical modifier keys, Rein maintains a state machine (Release → Active → Hold → Release) enabling shortcuts such as Ctrl+C, Ctrl+Shift+V, and Alt+Tab.
The modifier state is managed inside the trackpad interface and automatically cleared after completing a shortcut.
Every event transmitted over WebRTC DataChannels follows this structure:
interface InputMessage {
type:
| "move"
| "click"
| "scroll"
| "key"
| "text"
| "zoom"
| "combo"
| "touch"
| "copy"
| "paste"
| "ping"
| "pong"
dx?: number
dy?: number
button?: "left" | "right" | "middle"
press?: boolean
key?: string
keys?: string[]
text?: string
delta?: number
contacts?: TouchContact[]
timestamp?: number
}This format provides a platform-independent representation of every input event generated by the client and consumed by the host.