Per-key RGB lighting engine for ASUS ROG laptops — 50 hand-built effects, your own effects in JavaScript, music reactivity, and a premium desktop UI at a fraction of Armoury Crate's footprint.
~13 MB RAM · ~2% of one CPU core while animating · ~5% GPU with the window open · zero when the window is closed
The effect gallery and a live keyboard preview that mirrors the running effect in real time.
- Why
- Features
- The effects
- Custom effects (JavaScript)
- Tech stack
- Architecture
- Hardware protocol
- Known limitations
- Installation
- Usage
- Settings & customization
- Performance
- Development
- Contributing
- License
Armoury Crate's lighting stack idles at hundreds of MB for a handful of stock effects. Keyscape replaces it with a tiny always-on daemon plus an optional window — the per-key keyboard, lid logo and front light bar driven directly over HID, 50 effects you won't find anywhere else, and a scripting system so you can write (or have an AI write) your own.
It targets the ASUS "N-KEY" per-key keyboard (USB 0B05:19B6) found in
2021-and-newer ASUS ROG laptops. The lighting protocol is shared across that
whole family, and on startup Keyscape auto-detects your exact model's key
layout from ASUS's own data files — so it maps keys correctly on any such
laptop with no setup, falling back to a bundled layout if those files aren't
present.
- 50 unique effects in 7 categories — deliberately no static color, plain rainbow wave, breathing, or basic keypress ripple
- Write your own in JavaScript — drop a
.jsfile in, it appears in the gallery instantly; can't code? download the AI prompt and let any chatbot write it (details) - Everything parameterized — speed, intensity, palette (22 built-ins), key masks, plus per-effect controls; the UI builds every editor from the engine's schema, all editing live
- Playlist / shuffle — rotate any subset of effects on a timer
- Typing-reactive effects that see key positions only, with the input hook alive only while such an effect runs
- Music mode (strictly opt-in) — WASAPI loopback (what's playing, never the mic), FFT + beat detection off the render path, modulating the active effect rather than replacing it
- Deep customization — 8 accent themes, fonts, interface scale, sound themes, motion, transitions — all searchable in Settings
- First-run tour walking through every feature, replayable any time
- Live preview — the UI paints the exact wire bytes on the real key geometry, streamed from the daemon
- Vendor-service guard — detects Armoury Crate's LightingService, counters it with keepalive resends, and offers a reversible one-click permanent fix
- Near-zero cost — event-driven renderer, HID writes only when pixels change, 4 fps self-throttle on static scenes, capture threads that exist only while needed
| Category | Effects |
|---|---|
| Organic | Bioluminescence · Firefly Meadow (Kuramoto sync) · Aurora Veil · Thunderstorm · Ocean Tide · Ivy Growth · Coral Reef · Pollen Drift |
| Physics | Sandfall · Lava Lamp · Magnetic Poles · Pendulum Wave · Chaos Pendulum · Gravity Wells · Reaction Diffusion · Cloth Ripple |
| Cosmic | Meteor Storm · Pulsar · Supernova Cycle · Constellation · Black Hole · Solar Wind |
| Digital | Glitch Cascade · Bad Signal · Packet Flow · Game of Life · Rule Cascade · Bitcrush · Firewall |
| Typing | Typing Heatmap · Combo Meter · Echo Trails · Chain Lightning · Ink Splash · Tempo Pulse · Whack-a-Key |
| Ambient | Nebula Drift · Candlelight · Zen Garden · Moon Phases · Ink in Water · Solar Sync (real local time) · Deep Field |
| Kinetic | Swarm · Comet Billiards · Radar Sweep · Snake Trio · Spiral Bloom · Loom Weave · Orrery |
Every effect and its parameters are auto-documented in docs/effects.md.
Effects are plain .js files running on an embedded QuickJS engine inside the
core — nothing to install. A file is an EFFECT manifest plus a
render(req) function returning one [r,g,b] per key at ~30 fps, with the
user's palette, key taps and audio handed in each frame:
const EFFECT = { id: "my_waves", name: "My Waves", palette: "oceanic",
params: [{ key: "scale", label: "Scale", kind: "slider",
min: 0.5, max: 3, step: 0.1, default: 1 }] };
function render(req) {
return keys.map(k => {
const f = (Math.sin(k.cx * (req.params.scale * 3) + req.t) + 1) / 2;
const c = req.palette[Math.floor(f * 15)];
return [c[0] * f, c[1] * f, c[2] * f];
});
}Add it in the app's Custom tab (upload validates it and reports exact
errors), or drop it in %APPDATA%\Keyscape\effects\. A 60 ms per-frame
interrupt budget means a runaway script costs one aborted frame, not a hung
engine. Can't code? The Custom tab has a Download AI prompt button — a
self-contained spec file; paste it into ChatGPT / Claude / any AI with a
one-line idea, and upload what it writes. Full tutorial:
docs/js-effects.md, examples in
examples/js-effects.
| Layer | Built with |
|---|---|
Lighting core (keyscape-core.exe) |
Rust — no async runtime, ~6 lazily-spawned threads |
| Device I/O | hidapi — raw HID feature reports, no vendor SDK |
| Audio (music mode) | cpal — WASAPI loopback capture + a hand-rolled FFT |
| Custom effects | rquickjs — an embedded QuickJS engine |
| Control API | tungstenite — JSON over a loopback WebSocket |
| Tray, keyboard hook, autostart | windows-sys — direct Win32 |
Desktop shell (Keyscape.exe) |
Tauri 2 over the OS WebView2 |
| Frontend | Vite + TypeScript, zero runtime dependencies (~27 KB bundled) |
| Installer | Tauri's NSIS bundler; per-user, no admin |
| CI / releases | GitHub Actions on windows-latest |
Everything runs locally; the only network access anywhere is the loopback WebSocket between the two processes.
The defining decision is a two-process split:
┌────────────────────────────┐ ┌─────────────────────────────┐
│ Keyscape.exe (UI shell) │ │ keyscape-core.exe (daemon) │
│ Tauri 2 window │ WS │ effect engine @ 30 fps cap │
│ live preview canvas │◄──────►│ HID transport (dirty-block)│
│ gallery / custom / config │ 53971 │ QuickJS user effects │
│ runs only while open │ │ WASAPI loopback DSP │
└────────────────────────────┘ │ keyboard hook (on demand) │
│ LightingService guard │
│ tray icon · settings │
└──────────────┬──────────────┘
│ HID feature reports
┌──────────────▼──────────────┐
│ ASUS N-KEY device 0B05:19B6 │
│ 88 keys · logo · front bar │
└─────────────────────────────┘
Closing the window never interrupts the lighting. The window auto-starts the
core; the core's tray icon opens the window back up. The control API is JSON
over WebSocket on 127.0.0.1:53971 (loopback only) — see
core/src/ipc.rs for the ops. More in
docs/architecture.md.
Keyscape drives the ASUS N-KEY device (0B05:19B6) directly — no OpenRGB
server, Aura SDK, or vendor service at runtime. Reverse-engineered against this
exact machine and cross-checked with OpenRGB, asusctl's rog-aura and
g-helper:
| Command | Bytes | Purpose |
|---|---|---|
| Per-key color | 5D BC 00 01 01 01 <start> <count> 00 + RGB |
keyboard LEDs 0-166, 16 per packet |
| Aux color | 5D BC 00 01 04 00 00 00 00 + RGB×11 |
positional indices 167-177 (logo 167, front bar 169-174) |
| Brightness | 5D BA C5 C4 <0-3> |
must be nonzero or colors are invisible |
| Zone power | 5D BD 01 3F 0F 77 77 FF |
enables each zone per power state |
The 178-LED map comes from ASUS's own per-key CSV (under
ROG Live Service/DeviceContent/<model>/), extracted into
core/assets/layout_us.json. Two vendor-data bugs
are corrected: swapped LShift/LAlt scan codes, and a 1-based-vs-0-based aux
index (the lid logo is 167, not 168). Full details in
docs/protocol.md.
- The rear lid strip can't show live colors. It's a firmware-effect-only zone that ignores per-LED data and can't hold a color while the keyboard streams per-key frames (which it must, for effects) — the two are mutually exclusive on this controller, verified exhaustively. It's therefore off by default; the "Fixed color"/"Follow" options are experimental (they flash the board to paint it and won't persist). A color committed once via Armoury Crate does survive. The lid logo and front bar are unaffected.
- Non-ASUS laptops are not supported — the protocol and device ID are
ASUS-specific. Among ASUS ROG N-KEY laptops, Keyscape auto-detects the key
layout at startup (from
ROG Live Service/DeviceContent/), so other models should map correctly out of the box; if a model's data files are missing it falls back to the bundled layout, which may misplace a few keys until that model's layout is generated (Development).
Requirements: Windows 11 (or Windows 10 with WebView2, which is
preinstalled on 11), and an ASUS ROG laptop with the N-KEY per-key keyboard
(0B05:19B6). No admin rights needed; nothing is installed system-wide.
- Download
Keyscape_x64-setup.exefrom the Releases page. - Run it. It's a per-user NSIS installer (no UAC prompt) that places the app
and lighting core under
%LOCALAPPDATA%\Keyscapeand adds a Start Menu entry. - Launch Keyscape from the Start Menu. On first run it registers the lighting core to start at login, seeds the example custom effects, and shows a short welcome tour.
A portable zip (Keyscape-<ver>-windows-x64-portable.zip, with docs and
examples) is published alongside each release for people who'd rather not run
an installer.
Prerequisites: Rust (MSVC toolchain) and Node 20+.
git clone <this-repo-url> keyscape && cd keyscape
powershell -ExecutionPolicy Bypass -File tools/install.ps1install.ps1 builds the frontend, the core daemon and the shell, copies the
binaries to %LOCALAPPDATA%\Keyscape\bin, adds a Start Menu shortcut,
registers the core to start at login, and launches it. Re-run it any time
to update. To build just the distributable installer instead, run
tools/bundle-installer.ps1 (output in target/release/bundle/nsis/).
- If Armoury Crate is installed, go to Settings → ASUS lighting service → Disable service so it stops fighting Keyscape for the keyboard (reversible).
- Pick an effect in the gallery; tweak it live in the panel on the right.
Update by re-running the installer (or install.ps1). To remove everything:
Remove-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name Keyscape
Remove-Item "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Keyscape.lnk"
Remove-Item -Recurse "$env:LOCALAPPDATA\Keyscape"
Remove-Item -Recurse "$env:APPDATA\Keyscape" # settings + custom effects- Pick effects in the gallery; every parameter edits live
- Custom tab: upload, manage and delete your own
.jseffects - Tray icon (always there while the core runs): left-click opens the window, right-click pauses lighting or quits the core
- Core CLI:
keyscape-core --version | --list | --identify | --solid RRGGBB | --dump-docs | run <effect_id>.--zone-testmaps which LED index drives which physical zone.
Grouped General / ASUS service / Appearance & sound / Performance in the UI
with a search box, persisted at %APPDATA%\Keyscape\config.json. The full
list is in docs/settings.md. Highlights:
- Appearance — 8 accent themes (recolor the whole app live), fonts, interface scale, sound themes, motion, effect-transition length
- ASUS lighting service — while it runs, Keyscape counters it with 2 s keepalive resends; Disable service stops it permanently via one UAC prompt (reversible). Recommended.
- Music mode is off by default and only captures after you enable it
- Typing effects and autostart are toggleable; the input hook sees scan codes, not characters, and nothing leaves the process
- Rear bar — off by default (see Known limitations)
The current version shows in Settings → Performance → About.
| Metric | Value |
|---|---|
| Core RAM | ~13 MB |
| Core CPU (animating) | ~2% of one core (debug build measured; release lower) |
| Core CPU (static scene) | ~0 (4 fps idle tick, no HID writes) |
| UI GPU (window open) | ~5% of a 3D engine |
| UI when closed | not running |
| Full-board HID write | ~16 ms — which is why the cap defaults to 30 fps |
cd ui && npm run dev # frontend with HMR on :5173
cargo run -p keyscape-core # daemon (talks to real hardware)
cargo build --release # both binaries- Built-in effects live in
core/src/effects/<category>.rs; implement theEffecttrait, register anEffectInfo, and the UI picks it up automatically. User effects are JavaScript (see above). - Other models are automatic. The core parses your machine's own ASUS
per-key CSV at startup (under
%ProgramData%\ASUS\ROG Live Service\ DeviceContent\), so a different N-KEY laptop generally just works. To bake a model's layout into the repo as the bundled fallback, runnode tools/parse-layout.mjson that machine and commit the resultingcore/assets/layout_us.json.tools/gen-icon.ps1+tools/make-ico.mjsregenerate the app icon;keyscape-core --dump-docsregeneratesdocs/effects.md. - CI builds the workspace on Windows for every push
(.github/workflows/ci.yml); tagging
v*builds and publishes the installer + portable zip (.github/workflows/release.yml).
The looping preview at the top of this README isn't a screen recording — it's the real app UI, rendered headlessly and captured frame by frame, so it stays crisp and reproducible. The pipeline lives in tools/demo (capture.js, make-demo.ps1):
- Render the real UI.
capture.jsopens the Vite dev server (ui/, on:5173) in headless Chromium. Because the UI holds a live WebSocket to the lighting core, the on-screen keyboard preview animates exactly as the installed app does — it's the genuine effect, not a mockup. Effect params are set straight over the core's IPC, which lets us use exact values the UI sliders clamp (e.g.flip_rate: 0, below the slider's0.1minimum, which removes the effect's abrupt polarity flips). It then grabs 120 PNG frames at a steady cadence. - Loop it seamlessly. A drifting field never returns to its start, so a
naive loop hard-cuts and a ping-pong visibly reverses. Instead each frame is
cross-faded with its half-period counterpart —
out[i] = w·frame[i] + (1-w)·frame[(i + N/2) mod N]with the raised-cosine weightw = ½(1 - cos 2πi/N). Near the seam the frame is dominated by the half-shifted stream (continuous across the wrap), so motion stays continuously forward and the loop point is invisible. - Encode WebP. Animated WebP is full-colour, so the smooth gradients don't pick up the 256-colour dithering grain a GIF would — and the file lands around 1–2 MB instead of many times that.
Rebuild it with powershell -File tools/demo/make-demo.ps1 (needs the dev
server + core running, npm install in tools/demo, and ImageMagick).
Issues and PRs welcome. Keep commits atomic and
Conventional (feat:, fix:,
perf:, …), one concern per commit, working states only. New effects should be
genuinely distinctive — if it looks like a stock vendor effect, it doesn't ship.
MIT © 2026 Keyscape contributors