Because everyone deserves a little fun.
A small dose of joy for IDA Pro. Press Ctrl+Alt+T on any focused view
and the plugin picks a random effect or scene to surprise you with
-- a quick shake, a cartoon rage burst, a 30-second weather front rolling
across your disassembly, or a flock of IDA icons swooping over your
pseudocode. Something fun to lift the mood while you reverse engineer.
The pool is a fair shuffled deck: every registered animation plays once per round before any repeats, and the same one never fires twice in a row.
The idea -- and the name -- come from Ghidra, whose docking framework triggers "frustrated" emphasis animations when a user keeps hammering the same provider shortcut. This plugin started as a port of that snapshot/overlay model to IDA, and grew a second trick: long-running scenes you can keep working under. Manual invocation only, for giggles.
| Kind | Lives in | Lifetime | Interacts with widget? | Concurrency |
|---|---|---|---|---|
| Effect | effects/*.py |
one-shot (~400 ms - 1.5 s) | takes a snapshot; widget is briefly frozen under the overlay | one at a time (rapid presses are coalesced) |
| Scene | scenes/*.py |
long-running (~18-30 s) | non-blocking, low-alpha overlay; the widget stays fully interactive underneath | one active per widget -- triggering another scene on the same widget cancels it; scenes on other widgets keep running independently |
Shipped out of the box:
- Effects --
rage,rotate,shake,zoom. - Scenes (enabled by default):
weather-- a 30 s sunny -> stormy -> clearing story with drifting clouds, a sun, rain streaks, and lightning bolts.invasion-- 20 s flock of the host application's icon swooping across the widget in varied trajectories (a nod to Ghidra'sshowTheDragonOverComponent).invaders-- 30 s self-playing Space Invaders simulation: 5 x 11 alien grid, auto-firing player, bunkers, UFO flyovers, score HUD, rendered on a virtual 224 x 256 arcade canvas scaled to fit.lava_lamp-- 30 s of warm metaball-ish blobs rising and sinking in one of three palettes (warm orange, acid green, electric pink). Pure ambient mood.matrix_rain-- 25 s pure Matrix-style digital rain: dense katakana-flavoured glyph columns at varied speeds with a bright white leading glyph and a green fading tail. No watermark, no chrome -- just the rain.
- Scenes (shipped disabled -- rename off the
.disabledsuffix to enable):police-- 18 s red/blue siren bar with wig-wag / sync / chase flash patterns. The canonical example of the.disabledconvention below.fbi-- 18 s yellow/black caution-tape bars top & bottom, a wandering flashlight cone, and a sliding "FBI -- OPEN UP!" banner.nsa-- 25 s green glyph rain, rotating "CLASSIFIED / TOP SECRET / GHIDRA" watermark, and a pulsing REC indicator. Tribute to the NSA-birthed roots of Ghidra. (Superseded bymatrix_rainfor pure-rain vibe.)disco-- 22 s of swaying magenta/cyan/lime/gold spotlight cones pulsing to a beat, with a rotating disco ball and sparkles.terminal-- 25 s green-phosphor hacker console: fake nmap/ssh/root lines type out in monospace over a CRT scanline overlay, with occasional red[!!]alarm lines.aurora-- 28 s of slow cyan/green/magenta aurora ribbons undulating above a faint twinkling star field.
See it in action on X / @allthingsida.
The Hex-Rays plugin CLI can install directly from this folder (or a zip of it). From inside the repo:
hcli plugin install .Or point it at a specific location / archive:
hcli plugin install path/to/ida-frustrated
hcli plugin install ida-frustrated.zipStart IDA; the plugin shows up as IDA Frustrated under Edit > Plugins.
- Focus the view you want to decorate (Disassembly, Pseudocode, Graph, etc.).
- Press
Ctrl+Alt+T. You'll get a random effect or scene. - Press again for the next. Effects are one-shots; scenes run in the background for many seconds and can be cancelled by triggering another scene.
Effects live as individual Python files under effects/. On plugin load
every .py file in that folder is imported in alphabetical order and is
expected to register one or more effects at import time.
Paint callback signature:
paint(painter, snapshot, rect, progress, overlay)painter-QPainteralready configured with antialiasing enabled.snapshot-QPixmapgrabbed from the target widget at animation start.rect-QRectwhere the snapshot belongs, in overlay coordinates.progress-floatin[0, 1], pre-eased withInOutCubic.overlay- theEffectOverlay; calloverlay.erase_original(painter, rect)first if you want to clear the real widget before drawing.
Minimal example -- drop this as effects/flash.py:
from core import QtGui, register_effect
def _paint(painter, snapshot, rect, progress, overlay):
overlay.erase_original(painter, rect)
painter.drawPixmap(rect, snapshot)
alpha = int(180 * (1.0 - abs(progress - 0.5) * 2.0))
painter.fillRect(rect, QtGui.QColor(255, 255, 255, alpha))
register_effect("flash", 500, _paint)Restart IDA and flash joins the shuffle pool alongside the other
animations. Filename order only controls registration; play order is a
fair random shuffle.
Scenes live under scenes/. Same drop-in discovery, different registration
call and a different paint signature:
paint(painter, rect, elapsed_ms, overlay)painter-QPainterwith antialiasing; no snapshot is captured.rect- current target widgetQRect(re-read every frame, so it follows docking / resizing automatically).elapsed_ms- milliseconds since the scene started.overlay- theSceneOverlay. Stash per-run state onoverlay.state(initialized toNone); it's rebuilt on every fresh invocation.
Minimal example -- drop this as scenes/tint.py:
from core import QtGui, register_scene
_DURATION_MS = 10000 # 10 seconds
def _paint(painter, rect, elapsed_ms, overlay):
t = elapsed_ms / _DURATION_MS
painter.fillRect(rect, QtGui.QColor(120, 180, 255, int(90 * (1 - abs(t - 0.5) * 2))))
register_scene("tint", _DURATION_MS, _paint)Scenes are driven by a ~30 fps QTimer, follow the target widget's
geometry every tick, and self-destruct when their duration elapses or
the target widget disappears. Scenes are keyed per widget: triggering a
new scene on the same widget cancels that widget's current scene, but
scenes running on other widgets keep going independently.
At plugin load, core.load_animations() walks effects/ and scenes/
alphabetically and imports every .py file in each. Each imported
module registers itself with register_effect(...) or
register_scene(...) at import time — that's the only thing that puts
it into the rotation. Files whose names start with _ or . are
skipped (that's how scenes/_common.py stays out of the pool).
To take an animation out of the rotation without deleting it, rename
the file to add a .disabled suffix:
git mv scenes/police.py scenes/police.py.disabledThe loader only imports files that end in .py, so the renamed file
is skipped entirely — the module is never imported, it never calls
register_*, and it never enters the shuffled deck. Re-enable by
stripping the suffix (git mv scenes/police.py.disabled scenes/police.py).
From the IDA Python console:
import core
core.EFFECTS # registered effects
core.SCENES # registered scenes
core.frustrate() # play a random effect or scene on the focused widgetMIT -- see LICENSE. Written by Elias Bachaalany.