v0.1.0 ALPHA — © 2026 F-KEYS
RemapWrap serves a phone's browser a grid of keys that fire OS-level keystrokes on a PC. All traffic stays on the local network: no cloud, no account, no app installed on the phone.
Four parts: remapwrap-server.js, a Node.js process that serves the pages, relays WebSocket messages and injects keystrokes; dashboard.html, the PC side, where layouts are built; controller.html, the phone side, the keypad itself; and store.js, which keeps settings and profiles on disk.
RemapWrap began under the name F-Keys. The name moved to the company; the product kept the code.
| Requirement | Details |
|---|---|
| Node.js | v16.0.0 or higher. Download at nodejs.org |
| npm | Bundled with Node.js. v7+ recommended. |
| OS | Windows 10/11, macOS 11+, Ubuntu 20.04+ |
| Browser | Chrome or Edge (latest) for the dashboard |
| Ports | 7331 (HTTP) and 7332 (WebSocket) must be open |
| Requirement | Details |
|---|---|
| Browser | Chrome for Android, Safari for iOS, Firefox. Any modern mobile browser. |
| Network | Same Wi-Fi network as PC, or hotspot to PC |
| App install | None required |
The @nut-tree-fork/nut-js package compiles native bindings. Without it, RemapWrap runs in relay-only mode: presses reach the dashboard's live feed but no OS keystrokes fire. See nut-js install.
| OS | Required Tools |
|---|---|
| Windows | Visual Studio Build Tools 2019+ with "Desktop development with C++" workload + Python 3 |
| macOS | Xcode Command Line Tools: xcode-select --install |
| Linux | sudo apt install build-essential libxtst-dev |
RemapWrap runs from source, from the remapwrap/ directory of the f-keys repository:
2cd f-keys/remapwrap
The server prints its URLs on startup:
║ RemapWrap SERVER v0.1.0 RUNNING ║
╠══════════════════════════════════════════════╣
║ Dashboard: http://192.168.1.42:7331/
║ Controller: http://192.168.1.42:7331/controller
║ QR Code: http://192.168.1.42:7331/qr
║ WebSocket: ws://192.168.1.42:7332
╚══════════════════════════════════════════════╝
Open the Dashboard URL on the PC running the server. The dashboard is served to that machine only; a request for / from any other device is redirected to the controller.
Scan the QR code shown on the dashboard. The encoded URL carries the pairing secret, so a scanned phone is paired with nothing to type. A phone whose camera cannot scan opens the Controller URL directly and enters the six-digit PIN displayed on the dashboard. Five wrong PINs lock that address out for five minutes.
A tap on the phone fires the bound action as an OS-level keystroke on the PC. The dashboard's live feed records the key, the action fired, and the timestamp.
| File | Role | Runs On |
|---|---|---|
| remapwrap-server.js | HTTP server + WebSocket bridge + keystroke injector | PC (Node.js) |
| dashboard.html | Layout builder, pairing QR + PIN, live feed | PC browser, localhost only |
| controller.html | The keypad, served at /controller | Phone browser |
| store.js | Settings and profiles on disk | Inside the server |
| system.js | Command catalogue | Inside the server |
| licence.js | Tier check for the paid features | Inside the server |
The server injects the local IP and WebSocket port into controller.html at serve time; the dashboard reads them from /ip. Neither page carries a hand-edited address.
On disk: %APPDATA%\RemapWrap\settings.json holds pairing and the active profile; %APPDATA%\RemapWrap\profiles\*.json holds one file per profile. The folder is the import path — a profile file placed there appears in the dashboard.
The server carries one CONFIG block at the top of remapwrap-server.js. It is the only place a value needs changing: the dashboard and controller take their ports from the running server.
| Variable | Default | Description |
|---|---|---|
| HTTP_PORT | 7331 | Port for the HTTP server. Change if 7331 is in use. |
| WS_PORT | 7332 | Port for the WebSocket bridge. Must match dashboard + controller CFG. |
| DEV_MODE | true | Verbose console logging. Set false for silent production mode. |
| KEY_DELAY_MS | 30 | Delay between keydown and keyup in milliseconds. Increase to 60–80 if keystrokes miss in games. |
| MAX_CLIENTS | 8 | Max simultaneous WebSocket connections. Covers multiple phones + dashboards. |
No configuration. The controller is served with the server's IP and WebSocket port injected; the dashboard asks the server's /ip endpoint. Changing HTTP_PORT or WS_PORT in the server config is sufficient for all three parts.
The Action field in the key editor accepts a +-separated keystroke string. Each token resolves to an OS key via nut-js. The key table is generated from the nut-js enum at startup — all 137 keys the library can press are available — with aliases layered on top for the names people actually type.
f5
# Key combo
ctrl+c
# Three-key combo
ctrl+shift+s
# Media / system key
mute
| Family | Tokens |
|---|---|
| Modifiers | ctrl / control / lctrl / rctrl · shift / lshift / rshift · alt / lalt / ralt / altgr · win / cmd / super / meta |
| Navigation | esc, enter, tab, backspace, ins, del, home, end, pgup, pgdn, up, down, left, right, caps, prtsc, break |
| Function keys | f1 … f12 |
| Media | volumeup / volup, volumedown / voldown, mute, playpause / play, next, prev, stop |
| Punctuation | = plus - _ , . / ; ' [ ] \ ` ~ space |
| Numpad | numpadplus, numpadminus, numpadtimes, numpaddivide, numpaddot, numpadenter |
| Letters, digits | a–z, 0–9 |
Profiles are plain JSON files, one per file, in %APPDATA%\RemapWrap\profiles\. Exporting, sharing and importing a profile is moving that file. The format is versioned from the first byte: every profile carries "schema": 1 and holds pages, even when there is only one page.
"schema": 1,
"name": "Streaming",
"pages": [
{ "name": "Main", "cols": 12, "rows": 21,
"keys": [ // id, label, sub, action, color per key ] }
]
}
The dashboard's key editor writes every field a key can carry; a saved profile is its own reference for the full key shape.
Served at / to the machine running the server, and to that machine only — any other device requesting it is redirected to the controller. Pairing, licence and profile routes refuse non-local requests outright.
Shows the QR code and the six-digit PIN. The server detects its own LAN address; the QR re-renders when the address changes.
Clicking a key selects it; the key editor edits its label, subtitle, action string and background colour, and the canvas re-renders per edit. + ADD KEY appends, − REMOVE deletes the selection. Grid dimensions are per page.
Each press from the phone is appended with the key label, the action fired, and a timestamp. The feed is the loop's confirmation surface: a press that reaches the feed but moves nothing on screen is a nut-js problem, not a network one.
Sends the current layout to connected controllers over WebSocket. Edits do not reach the phone until pushed.
Served at /controller. Renders the pushed layout scaled to the phone's screen. The status badge reports connection state: LIVE, OFFLINE, CONNECTING….
A press animates, vibrates on Android, and sends the action over WebSocket. On a dropped connection the controller reconnects on its own; a reload is never required.
Two paths, one secret:
| Path | Mechanism |
|---|---|
| Scan the QR | The encoded URL carries the pairing secret. Nothing to type. The comparison is constant-time. |
| Type the PIN | For a phone that cannot scan: open http://[server-ip]:7331/controller and enter the six digits shown on the dashboard. |
Five wrong PINs lock the requesting address out for five minutes. A QR scan that reaches an error page is a firewall or network problem — see Firewall / Ports.
@nut-tree-fork/nut-js is a native module that requires compilation. If it fails to install, follow the steps for your OS:
# Download: https://visualstudio.microsoft.com/visual-cpp-build-tools/
# Select: "Desktop development with C++"
# 2. Install Python 3 (from microsoft store or python.org)
# 3. Configure npm
npm config set msvs_version 2019
# 4. Re-install
npm install @nut-tree-fork/nut-js
npm install @nut-tree-fork/nut-js
npm install @nut-tree-fork/nut-js
Keystroke injection: INACTIVE on startup.
RemapWrap uses two ports. Both must be reachable from the phone:
| Port | Protocol | Purpose |
|---|---|---|
| 7331 | TCP (HTTP) | Serves dashboard + controller pages, QR endpoint |
| 7332 | TCP (WebSocket) | Real-time keypress relay |
On first run, Windows may prompt you to allow Node.js through the firewall. Click Allow for private networks. If you missed the prompt, open Windows Defender Firewall → Allow an app → add node.exe with Private checked.
Some routers block device-to-device traffic on the same Wi-Fi (AP isolation / client isolation). A phone that cannot reach the PC on an otherwise working network is usually this. The router setting is the fix; a phone hotspot with the PC joined to it bypasses the router entirely.
Edit HTTP_PORT and WS_PORT in the CONFIG BLOCK at the top of fkeys-server.js. Update the matching WS_PORT and HTTP_PORT values in fkeys-dashboard.html's CFG block.
| Feature | iOS Safari | iOS Chrome |
|---|---|---|
| WebSocket | ✓ Supported | ✓ Supported |
| Touch input | ✓ Supported | ✓ Supported |
| Haptic vibration | ✗ Not available | ✗ Not available |
| Add to Home Screen | ✓ Works | ✓ Works |
| Background WebSocket | Drops after ~60s | Drops after ~60s |
iOS suspends a backgrounded WebSocket after roughly a minute. The controller reconnects on its own when the page returns to the foreground.
The server is not running, or it crashed on a port error. The terminal running npm start names the cause.
Firewall blocking port 7331, or the phone is on a different network. See Firewall / Ports.
Five wrong PINs lock that address for five minutes. Scanning the QR pairs without a PIN and is not subject to the lockout.
nut-js injects into whichever window has OS focus. The window to be controlled must be focused on the PC before keys are pressed on the phone.
Relay-only mode: @nut-tree-fork/nut-js did not load. The startup log prints Keystroke injection: INACTIVE. See nut-js install.
On EADDRINUSE the server prints the address of the instance that is probably already running, and exits. One server per machine; a genuinely occupied port means changing HTTP_PORT or WS_PORT in the CONFIG block.
| Area | State |
|---|---|
| Profiles on disk, multi-page layouts | Built. schema: 1, one JSON file per profile. |
| Pairing: QR secret + PIN with lockout | Built. |
| Command catalogue | Built. Everything offered runs; a test enforces both directions. |
| Packaged Windows build | Builds from source; unsigned, not yet distributed. |
| OBS / Twitch integrations (obs.*, stream.*) | Planned. Named in the catalogue, outside it until the integrations exist. |
| VU meters | Not built. |
| Row-column scanning | Not built; scanning is linear. |
Nothing in this table is aspirational. A feature listed as built has a test; a feature listed as planned is absent from the running product.
The product F-Keys was renamed RemapWrap; the F-Keys name became the company. Same alpha, carried forward:
+ Pairing secret in the QR URL; six-digit PIN fallback with lockout
+ Profiles on disk: %APPDATA%\RemapWrap, schema 1, pages
+ Dashboard restricted to localhost
+ Command catalogue with a two-way test
Initial alpha, under the working name F-Keys.
+ Layout builder, QR pairing, live feed
+ Touch keypad served to the phone
+ Auto-reconnect on both dashboard and controller
+ Layout export / import as JSON
+ Push layout from dashboard to phone over WebSocket
+ Haptic feedback on Android