Flash your Acer Nitro's RGB keyboard the moment a long-running task finishes — then watch it fade smoothly back to exactly the colours it had before.
Built for Claude Code, but the flash engine works from anything that can run a command.
You kick off a long task, alt-tab into a game, and then... you forget. Ten minutes later you check and it's been finished for nine of them.
Desktop notifications don't help — they're hidden behind a fullscreen game. Sounds get lost under game audio.
But your keyboard is right there, in your peripheral vision, always.
task finishes
│
▼
██████ ██████ ██████ ██████ ← 5 red blinks
▓▓▓▓ ▓▓▓▓ ▓▓▓▓ ▓▓▓▓ ← eased gradient back
░░░░ ░░░░ ░░░░ ░░░░
██████ ██████ ██████ ██████ ← exactly the colours you started with
- Restores your colours perfectly — reads the live per-zone colour off the hardware before flashing, then eases back into it. Your lighting ends up exactly as it was.
- Nothing runs in the background — no daemon, no service, no tray icon, no memory. The flash is spawned on demand and exits in ~4 seconds.
- One-time setup, then invisible — a single UAC prompt during install and never again. No console window ever appears.
- Zero configuration to start — one double-click detects your Python, your paths, and wires everything up.
- Fully tunable — colours, blink count, timing and fade length live in one small JSON file.
| Hardware | Acer Nitro or Predator laptop with a 4-zone RGB keyboard |
| Software | NitroSense installed and its service running |
| OS | Windows 10 or 11 |
| Python | 3.8+, with Add Python to PATH ticked |
Why Acer-only? The lighting is driven through Acer's
AcerGamingFunctionWMI interface. On other hardware the hooks still run, but the flash fails cleanly and logs why.
Set your keyboard to a static colour. While a NitroSense effect (Dynamic, Wave, Breathing…) is running, the flash cannot be seen — see Known limitation.
git clone https://github.com/AmmarHSufyan/NitroPulse.git
cd NitroPulseThen double-click setup.bat and accept the single UAC prompt.
That's the whole install. It detects your Python and paths, installs the one dependency, registers the tasks, wires up the hooks, and fires a test flash so you can see it work.
Finally, restart Claude Code (or open /hooks once) so it picks up the new hooks.
Undo everything at any time with uninstall.bat.
| Event | Fires when | Pattern |
|---|---|---|
done |
The task finishes | 5× red blinks → gradient fade back |
permission |
Approval is needed to run something | 4× red blinks → fade back |
idle |
You've been waiting on a prompt | white → yellow → white → fade back |
done is the one that matters while gaming — it fires the instant work stops, not after an
idle timeout.
The Acer WMI interface needs Administrator, but editor hooks run as an ordinary non-admin process. Doing the work directly in the hook would throw a UAC prompt in your face every single time — unusable.
The trick: a Scheduled Task registered with "highest privileges" can be started by a non-admin process without any UAC prompt. So the hook simply asks Windows to start the task.
task finishes
│
▼
Stop hook (non-admin, async — never blocks)
│
▼
nitro_trigger.py ──► schtasks /Run ──► ┌──────────────────────────────┐
│ ELEVATED · no prompt · no │
│ window │
│ │
│ read current zone colours │
│ blink red ×5 │
│ ease back over 1.2s │
│ exit │
└──────────────────────────────┘
You grant permission once, when the task is registered. Triggering it later just cashes in that stored consent — the same model any installed Windows application uses.
Timing: the hook returns in ~0.3s and is asynchronous, so nothing ever waits on it. The flash begins ~1s later and completes in ~5s.
Why the blinks snap but the return fades
The LED controller drops rapid successive writes, so a fast fade is nearly invisible — the intermediate colours never land. Snapping to full colour and holding it is unmistakable, even in peripheral vision.
The return is the opposite case. At ~1.2 seconds it's slow enough that every step renders, so it reads as a genuine gradient. It's eased with smoothstep, so it decelerates into the final colour instead of stopping dead — that soft landing is what makes it feel finished rather than abrupt.
The fade is also driven by the clock rather than a fixed step count. A WMI write is slow and its cost varies, so a fixed count would make the fade drift longer on a busy machine. Instead each iteration asks how far through it is now — a slow machine simply renders fewer intermediate colours and the fade still takes the time it was asked for.
Everything lives in src/nitro_config.json. Changes apply to the next
flash — nothing needs restarting.
{
"zones": [0, 1, 2, 3],
"base_color": [30, 30, 30],
"events": {
"done": {
"color": [255, 0, 0],
"blinks": 5,
"on_seconds": 0.22,
"off_seconds": 0.14,
"fade_back_seconds": 1.2
}
}
}| Key | Meaning |
|---|---|
zones |
Which zones light up, 0–3 (default: all four) |
base_color |
Fallback [R,G,B], used only if reading a zone's live colour fails |
color |
The [R,G,B] to flash |
blinks |
How many times to blink |
on_seconds / off_seconds |
How long the flash colour / your own colours are held |
fade_back_seconds |
Length of the eased gradient home. Set 0 to snap back instead |
Sequence events — flash through several colours
Instead of color + blinks, an event can step through a list:
"idle": {
"sequence": [[255, 255, 255], [255, 255, 0], [255, 255, 255]],
"hold_seconds": 0.3,
"gap_seconds": 0.0,
"repeat": 1,
"fade_back_seconds": 0.9
}| Key | Meaning |
|---|---|
sequence |
[R,G,B] colours to step through in order |
hold_seconds |
How long each colour is held |
gap_seconds |
If > 0, your own colours flash back between steps (this also skips the closing fade, since the run already ends on your colours) |
repeat |
How many times to play the whole sequence |
If your keyboard is running a NitroSense lighting effect — Dynamic, Wave, Breathing, Neon — the flash will not be visible. Use a static colour.
An effect runs on the keyboard's onboard controller, which takes ownership of the LEDs and ignores the per-zone RGB register entirely. Writes still succeed, and reads still echo them back — the register and the actual LEDs are simply decoupled while an effect is active.
NitroPulse detects this and tells you plainly in src/nitro.log:
NOTE: NitroSense effect 9 is active. The onboard controller owns the LEDs,
so this flash will NOT be visible.
Switch the keyboard to a static colour in NitroSense to see alerts.
Why this isn't worked around (and please don't "fix" it)
Acer exposes Get/SetGamingKBBacklight: a 15-byte struct whose first byte is the effect ID
(0 = static, 9 = Dynamic). The setter takes those 15 bytes prefixed with a leading 0,
16 total — the getter returns only the last 15, which is easy to get wrong.
Writing effect ID 0 does successfully seize the LEDs. This was tested: the keyboard becomes
fully controllable.
But writing the original struct back restores the mode register without restarting the animation. The keyboard stays frozen on its last colour until NitroSense is toggled by hand. The mode register and the effect engine are separate, and whatever re-arms the engine hasn't been found.
Seizing control is a one-way door. A notification that permanently killed your lighting the first time it fired would be far worse than one you can't see. So NitroPulse only ever reads the effect ID and warns.
Everything is logged to src/nitro.log. The flash runs with no console attached, so that
file is the only place errors surface.
| Symptom | Cause |
|---|---|
Log says effect N is active |
A NitroSense effect owns the LEDs — switch to a static colour |
Log says Flash failed |
Not an Acer, or NitroSense isn't running |
Log says task … did not start |
setup.bat hasn't been run on this machine |
| Log never appears at all | The editor hasn't reloaded its hooks — restart it |
| Worked before, stopped now | You moved or renamed the folder. Paths are absolute — just re-run setup.bat |
Fire the flashes by hand at any time with scripts\test_flash.bat — no admin needed.
NitroPulse grew out of a set of Acer Nitro RGB tools, and they're all still here. Use them directly, or as a reference for your own project — the WMI details below are the hard-won part.
| Tool | What it does |
|---|---|
src/keyboard_ui.py |
GUI colour picker for each of the 4 zones |
src/wallpaper_sync.py |
Matches your keyboard to your desktop wallpaper, with a watch mode |
src/screen_sync.py |
Real-time ambient lighting — samples your screen with DXcam and drives the keyboard at ~10fps |
python src/keyboard_ui.py # GUI
python src/wallpaper_sync.py --watch --interval 10 # follow the wallpaper
python src/screen_sync.py # ambient screen syncAll of these need Administrator. The scripts\ folder has self-elevating launchers.
The WMI details — everything you need to control an Acer Nitro keyboard yourself
Interface
| Namespace | root\WMI |
| Class | AcerGamingFunction |
| Method | SetGamingRgbKb(gmInput: UInt64) |
Colour packing — zone and colour go into one 32-bit value:
value = (B << 24) | (G << 16) | (R << 8) | zone_id
| Bits | Field |
|---|---|
| 24–31 | Blue |
| 16–23 | Green |
| 8–15 | Red |
| 0–7 | Zone ID |
Zone IDs are powers of two, not sequential:
| Zone | Position | ID |
|---|---|---|
| A | Far left | 1 |
| B | Centre-left | 2 |
| C | Centre-right | 4 |
| D | Far right | 8 |
IDs 0, 3, 5, 6 map to no physical zone.
Reading colours back: GetGamingRgbKb(zone_id) returns a UInt64 packed identically, so
R = (v >> 8) & 0xFF, G = (v >> 16) & 0xFF, B = (v >> 24) & 0xFF.
Other methods — SetGamingLEDColor, SetGamingKBBacklight, SetGamingLED,
SetGamingLEDBehavior all exist. They do not reliably set per-zone colours on this hardware;
SetGamingRgbKb is the one that works.
Admin is mandatory. Every one of these calls fails without elevation.
Is rapid colour switching bad for the LEDs?
No.
- LEDs switch in nanoseconds and are designed for PWM cycling at thousands of Hz
- Rated lifespan is 50,000–100,000 hours — 17 to 34 years at 8 hours a day
- Colour changes don't degrade LEDs. Only heat and overcurrent do, and both are hardware-limited by the onboard controller
- Your laptop will be replaced long before the LEDs dim
Practical update ceiling is the WMI → Embedded Controller → I²C chain, not the LEDs. NitroSense's own animations run at 15–30fps internally; 10–20 updates/sec per zone is comfortably safe.
What to actually avoid: flooding WMI in a tight loop with no sleep. Not for the LEDs' sake —
it burns CPU through WmiPrvSE.exe and can slow the Embedded Controller's other duties like fan
control.
I'd genuinely love to know. If NitroPulse is running on your machine, or you've built something on top of these WMI details, open an issue and tell me — what you're using it for, what hardware, what you changed.
It's not a requirement, just a request. Seeing where your work ends up is the best part of putting it out there.
Bug reports and pull requests are equally welcome.
MIT © Ammar Hassan Sufyan
Use it, change it, ship it commercially — just keep the copyright notice.