Skip to content

PoC: replace NutJS mouse/scroll with koffi FFI virtual input (Issue #130)#262

Closed
Muneerali199 wants to merge 2 commits into
AOSSIE-Org:mainfrom
Muneerali199:feat/virtual-input-ffi
Closed

PoC: replace NutJS mouse/scroll with koffi FFI virtual input (Issue #130)#262
Muneerali199 wants to merge 2 commits into
AOSSIE-Org:mainfrom
Muneerali199:feat/virtual-input-ffi

Conversation

@Muneerali199

@Muneerali199 Muneerali199 commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

Overview

This is a Proof of Concept prototype for Issue #130.

It demonstrates that replacing NutJS mouse/scroll input with direct OS-native virtual input APIs (via koffi FFI) is technically viable, before the approach is merged into the main codebase.


The problem with NutJS

NutJS synthesises input at the application layer (X11, AT-SPI, Windows UI Automation). This means:

  • Wayland compositors reject or ignore the events
  • The OS pointer acceleration curve is bypassed
  • Secure desktop sessions block application-level injection

The PoC approach

Call the OS virtual-input API directly so events enter the kernel's input pipeline as if they came from real hardware:

Mobile (Rein client)
    |  WebSocket { type: "move", dx: 5, dy: 3 }
    v
InputHandler.ts  →  VirtualInput.ts (platform factory)
    |
    |── Windows  → user32.dll  SendInput(MOUSEEVENTF_MOVE)
    |── Linux    → /dev/uinput  write(EV_REL | REL_X, REL_Y)
    └── macOS    → CoreGraphics  CGEventPost(kCGEventMouseMoved)
                                        |
                                        v
                              OS kernel input pipeline
                   (acceleration, gestures, display-protocol routing)

PoC files

File Purpose
poc/virtual-input.cjs Standalone demo — run directly with node, no compilation needed
poc/README.md Architecture, comparison vs NutJS, per-platform run instructions
src/server/VirtualInput.ts TypeScript driver with VirtualInputDriver interface
src/server/InputHandler.ts Integration — wires WebSocket messages → VirtualInput

Running the PoC

npm install
node poc/virtual-input.cjs

The script pauses 3 seconds (switch to any window), then:

  1. Moves mouse +150px right/down, then back
  2. Left click, right click
  3. Scroll up 3 ticks, scroll down 3 ticks
  4. Horizontal scroll right
  5. Pinch zoom in / out (Ctrl + scroll)

Tested and confirmed working on Windows 10 — all 9 operations pass.

On Linux: sudo node poc/virtual-input.cjs (or add user to input group).


Platform coverage

Operation Windows Linux macOS
Relative mouse move MOUSEEVENTF_MOVE EV_REL REL_X/Y kCGEventMouseMoved
Left / right / middle click MOUSEEVENTF_LEFT/RIGHT/MIDDLEDOWN/UP EV_KEY BTN_* kCGEventLeftMouseDown/Up
Press-only / release-only (drag) same flags, split same codes, split same, split
Vertical scroll MOUSEEVENTF_WHEEL EV_REL REL_WHEEL CGEventScrollWheelEvent wheel1
Horizontal scroll MOUSEEVENTF_HWHEEL EV_REL REL_HWHEEL CGEventScrollWheelEvent wheel2
Pinch zoom Ctrl + WHEEL Ctrl + REL_WHEEL Ctrl + ScrollWheel

Changes vs NutJS

NutJS This PoC
Layer Application (X11 / AT-SPI) Kernel (uinput / SendInput / CGEventPost)
Wayland support No Yes
OS acceleration Bypassed Applied
Admin rights No No (Win/macOS); input group (Linux)

CodeRabbit review — all 6 issues addressed

Issue Fix
poc/virtual-input.cjs run command referenced wrong filename Fixed .js.cjs
Click handler ignored msg.press (no drag support) press=true → down-only, press=false → up-only, undefined → full click
vi.init() in constructor could throw and crash server start Wrapped in try/catch; vi set to null on failure; all call sites guard null
cleanup() destroyed device while timers could still fire cleanup() now cancels timers, sets isShuttingDown flag; callbacks guard it
macOS scrollH was a no-op Fixed: CGEventCreateScrollWheelEvent(src, 1, 2, 0, ticks) with wheel2
wss.on("close") never fires for noServer:true WSS Changed to server.on("close") on the HTTP server

Checklist

  • npx tsc --noEmit — zero new errors
  • npx biome check --write src/ — clean
  • npm run build — passes
  • PoC tested on Windows 10 — all 9 operations confirmed working
  • All 6 CodeRabbit review comments addressed

Summary by CodeRabbit

  • New Features

    • Added a cross-platform virtual input implementation enabling synthetic mouse movement, clicks, scrolls, and pinch/zoom gestures.
  • Improvements

    • Replaced prior input path with the new virtual input system for more reliable OS-level event synthesis across Windows, Linux, and macOS.
    • Added automatic cleanup on server shutdown to release input resources.
  • Documentation

    • Added a PoC guide describing usage and platform notes.

…OSSIE-Org#130)

- Add koffi@2.15.1 as production dependency
- Add src/server/VirtualInput.ts: platform-native VirtualInputDriver interface
  with Windows (user32.dll SendInput), Linux (/dev/uinput), macOS (CoreGraphics)
  implementations; createVirtualInput() factory selects driver at runtime
- Rewrite move/click/scroll in InputHandler.ts to use VirtualInput; NutJS
  retained only for keyboard actions (key, combo, text, copy, paste, zoom modifier)
- Add InputHandler.cleanup() and call it in websocket.ts on wss close
- Add poc/virtual-input.cjs: standalone runnable PoC (node poc/virtual-input.cjs)
  covering all 9 operations; tested and confirmed working on Windows 10

Closes AOSSIE-Org#130
@coderabbitai

coderabbitai Bot commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

This PR replaces NutJS-based mouse handling with a cross-platform virtual input driver (FFI via koffi), adds a TypeScript VirtualInput implementation for Windows/Linux/macOS, integrates it into InputHandler (move/click/scroll), provides a PoC script and README, and wires cleanup on server close.

Changes

Cohort / File(s) Summary
Package Dependencies
package.json
Added koffi (^2.15.1) dependency for native FFI bindings.
Virtual Input Implementation
src/server/VirtualInput.ts
New module exporting VirtualInputDriver interface and createVirtualInput() factory with Windows (SendInput), Linux (/dev/uinput), and macOS (CoreGraphics) backends implementing init, moveMouse, scrollV/scrollH, click/down/up, and cleanup.
InputHandler Integration
src/server/InputHandler.ts
Replaced NutJS mouse APIs with the VirtualInput driver (vi field), added cleanup() and shutdown handling, rerouted move/click/scroll/zoom to vi methods, kept NutJS for keyboard paths.
Server lifecycle hook
src/server/websocket.ts
Added HTTP server "close" handler to call inputHandler.cleanup() and emit shutdown log.
Proof of Concept & Docs
poc/virtual-input.cjs, poc/README.md
Added standalone PoC demonstrating platform-specific virtual input behavior and README with usage, architecture, and platform notes.

Sequence Diagram

sequenceDiagram
    participant Client
    participant WebSocket as WebSocket Server
    participant InputHandler
    participant VirtualInput as VirtualInput Factory
    participant Platform as Platform Driver

    rect rgba(100, 150, 200, 0.5)
    Note over Client,Platform: Initialization
    WebSocket->>InputHandler: new InputHandler()
    InputHandler->>VirtualInput: createVirtualInput()
    VirtualInput->>Platform: detect OS & instantiate driver
    Platform->>Platform: init platform resources
    Platform-->>VirtualInput: driver ready
    VirtualInput-->>InputHandler: VirtualInputDriver instance
    end

    rect rgba(150, 200, 100, 0.5)
    Note over Client,Platform: Input Operations
    Client->>WebSocket: trackpad/input event
    WebSocket->>InputHandler: handleInput()
    InputHandler->>Platform: moveMouse(dx, dy)
    Platform->>Platform: emit movement event
    InputHandler->>Platform: leftClick()
    Platform->>Platform: emit button event
    InputHandler->>Platform: scrollV(ticks)
    Platform->>Platform: emit scroll event
    end

    rect rgba(200, 100, 100, 0.5)
    Note over Client,Platform: Cleanup
    WebSocket->>InputHandler: cleanup() [on close]
    InputHandler->>Platform: cleanup()
    Platform->>Platform: release device resources
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Typescript Lang, Documentation

Poem

🐰 Hop hop, the old mouse sleeps,

I stitch events in kernel heaps.
Windows, Linux, macOS cheer,
FFI whispers, inputs appear.
A tiny rabbit taps — click, scroll, and steer.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: replacing NutJS with koffi FFI virtual input and references the corresponding issue.
Description check ✅ Passed The description comprehensively covers the PoC approach, implementation details, testing results, and platform coverage. However, the functional verification checklist is not completed—only the overview and technical details are provided.
Linked Issues check ✅ Passed The PR fully addresses issue #130 requirements: provides a working PoC with platform-specific virtual input drivers (Windows, Linux, macOS), demonstrates native gesture handling without gesture-by-gesture implementation, includes cleanup on shutdown, and covers trackpad operations across platforms.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #130 objectives. The PR replaces NutJS with koffi FFI for trackpad input only, retains NutJS for keyboard, adds platform-specific virtual input drivers, includes comprehensive PoC documentation, and wires cleanup on server shutdown.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@poc/virtual-input.cjs`:
- Around line 17-23: Update the run/instructions comment at the top of
poc/virtual-input.cjs so it references the correct filename: replace the
incorrect "node poc/virtual-input.js" (and any other occurrences of
"virtual-input.js") with "node poc/virtual-input.cjs" so the documented command
matches the actual file name.

In `@src/server/InputHandler.ts`:
- Around line 144-159: The click handler currently ignores InputMessage.press
and always emits a full down+up via
this.vi.leftClick()/rightClick()/middleClick(), breaking press/release/drag
flows; update the case "click" branch to validate msg.button as now, then if
msg.press === true call the corresponding down method (e.g.,
this.vi.leftDown()/rightDown()/middleDown()), if msg.press === false call the
corresponding up method (e.g., this.vi.leftUp()/rightUp()/middleUp()), and only
call the full click methods (this.vi.leftClick()/rightClick()/middleClick())
when msg.press is undefined (meaning a normal click). Ensure you reference
VALID_BUTTONS, msg.button, msg.press and replace the single try/catch around the
action to preserve error handling.
- Around line 41-44: InputHandler currently calls createVirtualInput() and
this.vi.init() during construction, which can throw and crash createWsServer;
wrap the init call in a try/catch inside InputHandler's constructor (or an init
method) so failures to open /dev/uinput are caught, log the error, and mark the
virtual input as unavailable (e.g., set this.vi = null or this.viAvailable =
false). Also update any methods that use the virtual input (handlers for
move/click/scroll or any calls to this.vi.*) to first check the availability
flag or null before invoking, and return/fail gracefully when the virtual input
is not initialized. Ensure logging gives context (init failed) and does not
rethrow so server startup continues.
- Around line 51-54: The cleanup() method currently only calls this.vi.cleanup()
but must cancel any pending move/scroll work first to avoid timers firing after
the device is released; update cleanup() to clear and nullify this.moveTimer and
this.scrollTimer (and any related queued work), and set/flip a shutdown flag
(e.g., this.isShuttingDown) or check the timers' existence inside the
move/scroll handlers so callbacks return early if shutdown is in progress, then
call this.vi.cleanup(); ensure references to moveTimer/scrollTimer and the
move/scroll handler functions are updated so no further access to the destroyed
vi occurs.

In `@src/server/VirtualInput.ts`:
- Around line 389-398: The scrollH implementation in scrollH(_ticks) drops the
horizontal delta by emitting a zero-delta wheel event; change it to create a
two-axis scroll event using CGEventCreateScrollWheelEvent with wheelCount=2 and
pass the horizontal delta as the second wheel argument (wheel2) instead of 0,
converting _ticks to the expected integer type; keep using the same source,
CGEventPost(kCGHIDEventTap, ev) and CFRelease(ev) flow and ensure the vertical
wheel argument stays correct (or 0 if unused) so horizontal two-finger trackpad
deltas forwarded from InputHandler are delivered on macOS.

In `@src/server/websocket.ts`:
- Around line 67-70: The wss "close" handler never runs because wss is created
with noServer: true and never closed; ensure inputHandler.cleanup() is invoked
during normal shutdown by wiring server shutdown to close the wss or call
cleanup directly. Add a listener on the HTTP/HTTPS server (the server that calls
server.on("upgrade", ...) to use wss) for its "close" event and call either
wss.close() to trigger the existing wss.on("close", ...) or directly call
inputHandler.cleanup(), so that inputHandler.cleanup() runs on normal server
shutdown (referencing wss, the server's "close" event, and
inputHandler.cleanup()).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 54a6d277-da31-4707-ae09-3f091c5ef0bc

📥 Commits

Reviewing files that changed from the base of the PR and between efabeb0 and 7948551.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • package.json
  • poc/virtual-input.cjs
  • src/server/InputHandler.ts
  • src/server/VirtualInput.ts
  • src/server/websocket.ts

Comment thread poc/virtual-input.cjs
Comment thread src/server/InputHandler.ts Outdated
Comment thread src/server/InputHandler.ts
Comment thread src/server/InputHandler.ts
Comment thread src/server/VirtualInput.ts Outdated
Comment thread src/server/websocket.ts Outdated
- poc/virtual-input.cjs: fix run command in header comment (.js -> .cjs)
- VirtualInput.ts: add leftDown/leftUp, rightDown/rightUp, middleDown/middleUp
  for press-only / release-only drag support on all three platforms
- VirtualInput.ts: fix macOS scrollH — use CGEventCreateScrollWheelEvent with
  wheelCount=2 and wheel2 arg so horizontal scroll is no longer a no-op
- InputHandler.ts: click case now respects msg.press (true=down, false=up,
  undefined=full click) enabling drag flows
- InputHandler.ts: wrap vi.init() in constructor try/catch; server startup
  continues with vi=null if virtual device cannot be opened, all vi call
  sites guard against null
- InputHandler.ts: cleanup() cancels pending move/scroll timers and sets
  isShuttingDown flag before destroying the device; timer callbacks guard
  isShuttingDown so no event fires after cleanup
- websocket.ts: replace wss.on('close') (never fires for noServer:true WSS)
  with server.on('close') on the underlying HTTP server so cleanup runs
  reliably on Vite dev server shutdown
- poc/README.md: add architecture diagram, comparison table vs NutJS,
  per-platform run instructions, and operation coverage table
@Muneerali199 Muneerali199 changed the title feat(input): replace NutJS mouse/scroll with koffi FFI virtual input (Issue #130) PoC: replace NutJS mouse/scroll with koffi FFI virtual input (Issue #130) Mar 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant