Winamp Modern (.wal) Skin Engine
SkillCloud & infraWinamp 5.x .wal skin engine — Wasabi XML/XUI renderer, MAKI bytecode VM, VFS mounts, component hosting, and the ClassicPro engine import. Use when working on the WinampModern subsystem, debugging a .wal skin that fails to load or renders wrong, or extending Wasabi/MAKI coverage, or triaging compatibility across many skins at once.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Winamp Modern (.wal) Skin Engine skill
What this skill tells your AI
The instructions your AI receives, as published by ad-repo/nullplayer in skills/winamp-modern-skin-guide/SKILL.md and read by ahel’s review.
NullPlayer's fourth UI mode (PlayerUIMode.winampModern) loads and runs Winamp 5.x modern skins —
.wal archives containing Wasabi XML/XUI markup and compiled MAKI bytecode. It is a clean-room
implementation: the archive is parsed, the object graph is built, and the scripts are interpreted by
NullPlayer's own code. No Winamp binary, plugin, or asset is bundled.
User-facing name: Modern. This family is presented in the skin menu as Modern; NullPlayer's
own former Modern/Metal families are shown as Original/Original-Metal. Every internal
identifier is unchanged — the enum case and persisted raw value stay winampModern, as does the
-uiMode winampModern flag. Never rename compatibility identifiers, preference keys, type names, or
references to the actual stock Winamp Modern skin.
The runtime loads, scripts, and renders real skins, but see compatibility.md for the exact supported/unsupported surface before assuming any behavior works.
The rule that outranks everything below
Winamp Modern must never change how Classic or Original skins behave. Those modes work; this one is the one under construction, and a regression there is a regression in the part of the app people already rely on.
The trap is not import — that is already covered by the ModernSkin//Windows/Modern*/ rule in
CLAUDE.md. It is shared code every mode runs, App/WindowManager.swift above all. Adding
behavior there and reasoning that it "should be a no-op for the other modes" is not good enough, and
has already produced one live regression (B56: a screen clamp added for .wal window placement moved
Classic's sub-windows too). Gate on the mode explicitly —
uiMode.controllerFamily == .winampModern — so the other modes run the identical code path they ran
before, and the claim is enforced by the compiler rather than by an argument.
The rule forbids side effects, not deliberate fixes. It protects Classic from being changed as a
consequence of .wal work; it does not mean a bug in shared code can never be fixed. 2026-09-03: the
.wal video pass correctly kept the end-of-media session leak out of its own change — an additive
flag only the .wal host reads — but the underlying bug hit Classic too (B107), and its sibling B108
killed Plex/Jellyfin/Emby finish-scrobbling and video-playlist advance in every mode. Fixing those
necessarily changed Classic, and that was legitimate as its own scoped change with the impact stated
up front. Do not let the rule turn a shared bug into one nobody is allowed to fix — say plainly that
it is separate work and let the user decide.
Three corollaries follow from it, each of which has already cost a phase. They are one line here and a worked example in reference/harness.md:
- A structural probe is not a picture.
surfaces=1says a client area exists, never where it is drawn. Render it or run it before handing it over — §A structural probe is not a picture. - A number that moved is not the symptom that was reported. Reproduce the reporter's own steps end to end, and drive the repro yourself — §A number that moved is not the symptom that was reported.
- Verify window geometry in the running app, not in your head.
WINAMP_MODERN_PLACE_TRACE=1is the probe — §Verify window geometry in the running app.
Pick your working mode first
Coverage is demand-driven and the wild corpus is effectively unbounded, so the unit of work matters as much as the code. Three modes, three different entry points:
| You are… | Start with | Not |
|---|---|---|
| Debugging one named skin | skins.md → skins/<skin>.md, then reference/harness.md | reading this file top to bottom |
| Measuring a skin — what does it contain, what works, how good is it? | /wal-skin-report <skin.wal> (skills/wal-skin-report) — fixed measurement order, structured report, A–F grade with a confidence axis | ad-hoc dumps whose findings evaporate |
| Deciding what to implement next across many skins | triage-playbook.md — corpus measurement, defect classes, the demand index | fixing whatever the last bug report named |
The two habits those exist to break: a skin is a test case, not a milestone (batch work by capability, not by skin — one fix that unblocks 200 skins beats ten that unblock one), and a skin's default state is not the skin (Defix ships eight display styles; the cassette is one of them, and the animated VU meters behind the other eight have never been rendered here).
Routing: which file answers this?
This file is a router. Find your symptom below and read that one focused reference — not this file top to bottom. Rows are grouped by area; within a group, follow the most specific row.
Loading, geometry, and where windows open
| Symptom / question | Read |
|---|---|
Load, mounts, @VARS@, include/glob, sibling skins | reference/loading.md |
| Geometry, anchors, y-origin, collapsed windows | reference/loading.md |
| The bundled default skin, or Modern mode has nothing to load | reference/loading.md → The bundled default skin |
A window opens at its minimum_* rather than the size its skin declares | reference/loading.md → default_w/default_h are <container> attributes too |
| A component window (visualizer, video) opens as a sliver | reference/loading.md → A component window with no stated size |
| Skin starts in an impossible all-zero settings state | reference/loading.md → settings must start in a state scripts can express |
| Where a skin's windows open, overlapping windows, the tiling | reference/components.md |
| Skin-opened window appears in the wrong place | reference/components.md → default_visible |
| Several windows in the menu share one name | reference/components.md → default_visible, and WinampModernContainerTopology.menuLabels |
| A window the skin closes at startup is missing from every probe | reference/components.md → visible on a container answers two questions |
| Window restores at the wrong size or one skin inherits another's frame | reference/rendering.md → A .wal window's size is still the skin's |
| Container/layout writes do not move or size their window | reference/rendering.md → A container's x/y/w/h are its window's |
| A bare outline rectangle opens beside the player (drop shadow, snap preview) | reference/components.md → A window that only fakes a Windows desktop effect |
Drawing: sprites, colour, text
| Symptom / question | Read |
|---|---|
cfgattrib, onActivate, alpha, fill, sliders, ProgressGrid, animation; album art, animated layer or image parameter draws stale/wrong | reference/rendering.md |
| Toggle works but never looks active | reference/rendering.md → onActivate |
| Playlist/EQ/library lamp on a skin button is inverted or counts clicks | reference/rendering.md → A TOGGLE button's lamp |
| Shuffle/repeat/crossfade disagree with the host | reference/rendering.md → Some cfgattrib values are the host's |
| Vertical slider uses the wrong axis or EQ curve is absent | reference/rendering.md → A skin spells the axis two ways |
| Bitmap icons/borders look blurry at an integer UI Size | reference/rendering.md → Bitmap interpolation follows UI Size × backing scale |
| A window has no border or background, or a skin's own labels are invisible against it | reference/rendering.md → A standard frame whose artwork stayed in Winamp |
Settings window is an empty slab, or a <Wasabi:TitleBox> shows neither label nor body | reference/rendering.md → <Wasabi:TitleBox> is a body, not just a border |
| Settings page has boxes but no switches, labels, sliders or drop-downs | reference/rendering.md → The Wasabi standard form widgets are the primitives they wrap |
| A skin-owned right-click menu is missing or wrong, or a menu a double-click opens never appears | reference/rendering.md → A skin's own right-click menus |
| Colour resolution, themes, unreadable selections/titles | reference/rendering/colour.md |
| White/black slab appears where a named colour belongs | reference/rendering/colour.md → How a colour resolves |
| Selected row or title text matches its background | reference/rendering/colour.md → A resolved colour is not yet readable |
| A title draws as a smear, or two objects in one slot both draw, or artwork ignores window focus | reference/rendering/colour.md → activealpha/inactivealpha |
A <gradient> fills flat instead of ramping | reference/rendering/colour.md → a <gradient> with no direction |
| The playing playlist row has no marker, or the selection bar never appears | reference/rendering/colour.md → A marker only marks when it differs |
| Theme picker is empty or will not switch | reference/rendering/colour.md → The picker |
| A skin's colours are readable-but-bad and the user wants to fix them by hand | reference/rendering/colour.md → A user override outranks the chain |
| Text metrics, fonts, clocks, bitmap fonts, missing height, offsets | reference/rendering/text.md |
A paragraph draws as one clipped line, or wrap=/<Wasabi:Text> layout | reference/rendering/text.md → A paragraph is not a line |
| Playlist/library/tab text draws in a console font, or a size too small | reference/rendering/text.md → Host-drawn text is not skin-declared text |
| Clock fields collide or the separator sits off baseline | reference/rendering/text.md → A clock is a run of fields |
| A separator or icon a script placed sits on the string beside it | reference/rendering/text.md → getTextWidth() carries the box's own margin |
| A readout's glyphs are chopped at the bottom, or sit low in their box | reference/rendering/text.md → A line is centred in the cell the skin declared |
Splitter cursor/drag/persistence or <Wasabi:Frame> | reference/rendering/frame-splitter.md |
| Slow rendering, CPU, repaint storms | reference/performance.md |
Input and hit testing
| Symptom / question | Read |
|---|---|
Dead mouse target, clipping, regions, drag policy, sysregion | reference/rendering/hit-testing.md |
| Invisible layer blocks clicks or drags the window | reference/rendering/hit-testing.md → Hit testing |
| A control is dead because a second object shares its rect, or a skin declares the same id twice | reference/scripting.md → getObject skips a duplicate id that never came up |
A button draws, presses and glows but runs no command; setText/search terms disappear or a search action receives empty terms | reference/scripting.md → embed_xui — the wrapper is the control |
| Control works once, hides itself, and cannot be clicked again | reference/scripting.md → A layout must not be left with no way to seek |
| Config/EQ drawer or custom list will not scroll | reference/scripting.md → The mouse wheel is a layout event |
Scripting (MAKI)
| Symptom / question | Read |
|---|---|
Script abort, arity, unknown method, script-built UI; host readout or EQ change never reaches a script; keyboard, mouse wheel, wrapper value, scrolling; getAutoWidth/getAutoHeight and scripted layout drift | reference/scripting.md |
| A call trace stops mid-handler with no failure line | reference/scripting.md → An event handler is also a method |
| Window jumps to a screen corner when a skin panel opens, or a skin's window chrome drifts away from the window's content | reference/scripting.md → Writing back the position a window just read |
| A window a skin's own script closed never comes back, or opens empty | reference/scripting.md → onSetVisible — a window a script closes has to be reopened |
| One window's script sets something in another window and nothing happens | reference/scripting.md → getLayout() answers NULL for a layout that has never been shown |
| Dragging one window should pull another along and does not | reference/scripting.md → onMove() is dispatched to the window objects only |
Components and hosted surfaces
| Symptom / question | Read |
|---|---|
Playlist/EQ/library hosting, synthesis, topology; TOGGLE, container ids, first layout, default_visible; NullPlayer-hosted text size or palette; an auxiliary window freezes, repaints wrong, or leaks on teardown | reference/components.md |
hold="none", flat holder slab, component routing | reference/components.md → Component hosting |
| A window opens as the skin's own frame around an empty hole | reference/components.md → A frame the skin drew and left for Winamp to fill |
| Component bucket/thinger, or a skin declares a widget that never enters its include closure | reference/components.md → The component bucket |
| Hosted surface survives the wrong tab or remounts dead | reference/components.md → Unmounting is not teardown |
| Mode switch teardown crashes or leaks a hosted surface | reference/components.md → Teardown order |
| Embedded playlist/library text sizes disagree | reference/components.md → How large NullPlayer draws its own text |
A skin's own About page, skin.about.group, the About GUID | reference/components.md → The About page is a group, not a window |
Visualization, video, browser, notifier
| Symptom / question | Read |
|---|---|
<vis> analyzer/oscilloscope, gain, colours, modes | reference/rendering/vis.md |
| Visualization timing, stepped motion, pause freeze | reference/performance.md → The visualization has a clock of its own |
| AVS/MilkDrop component holder or embedded visualization | reference/components/visualization.md |
| Plugin pane draws an analyzer and the user wants something else in it | reference/components/visualization.md → An unhosted pane is a surface with a choice of its own |
| Several visualization holders show the wrong engine | reference/components/visualization.md → one holder per skin |
| Video picture, child-window sizing, control bar | reference/components/video.md |
| Clock or transport frozen while a video plays | reference/components/video.md → The picture's clock |
| Video window pops out when a tab changes | reference/components/video.md → A holder leaving is a tab switch |
| Browser/WebKit, navigation, search URL, duplicate toolbar | reference/components/browser.md |
| Scheme-less web address is mistaken for a VFS path | reference/components/browser.md → four navigation routes |
| Notifier toast text, layout, visibility, timing | reference/components/notifier.md |
| Notifier title is invisible or rows overlap | reference/components/notifier.md → text and layout |
The compatibility surface, and the other engines
| Symptom / question | Read |
|---|---|
Supported Wasabi markup; slider action families and onSetPosition; playlist/visualization/video toolbar actions | compatibility/wasabi-surface.md |
| Host action is accepted but deliberately inert | compatibility/wasabi-surface.md → action families |
| Implemented MAKI method/events | compatibility/maki-surface.md |
| Security model, limits, policy, verification status | compatibility/limits-and-policy.md |
| ClassicPro engine/import behavior | reference/classicpro.md |
| WACUP probe, branding branch, WACUP-only surface | reference/wacup.md |
Instruments, proof, and what to work on
| Symptom / question | Read |
|---|---|
| Probes, env vars, dumps, live defect | reference/harness.md |
| Probe reports no match or no event | reference/harness.md → A blind instrument reads as a working feature |
| Whole skin dead or startup handler aborts | reference/harness.md → The order that made Phase 33 cheap |
| Meter moves too little | reference/harness.md → histogram the frames |
| GUI-only scripted-control report | reference/harness.md → Ask for the live trace first, not fourth |
| Frame is fast but the app hangs | reference/harness.md → Profiling the running app |
| Renderer regression proof | reference/harness.md → The golden images |
| Proving an engine-wide change broke no other skin | reference/harness.md → The corpus render sweep |
| Measure one skin end to end | /wal-skin-report <skin.wal> |
| The GUI verification pass before handing work over | manual-qa-checklist.md |
| One named skin's current state | skins.md → skins/<skin>.md |
| Choose the next cross-skin capability | triage-playbook.md, then the ranked Reach table in TASKS.md |
| Current open work | TASKS.md — the only live backlog; closed history is the archive |
The backward-compatibility map for section-title pointers in old handoffs lives in
docs/winamp-modern/section-title-map.md.
Routing rules:
- Follow the most specific row. The parent
rendering.mdowns drawable behavior that does not belong to hit testing, visualization, splitters, text, or colour; the parentcomponents.mdowns hosting core that does not belong to video, visualization, browser, or notifier surfaces. - A visual symptom and its evidence can route to different files. For example, a dead control's
semantics live in hit testing or scripting, while the command that proves which object won the
point lives in
reference/harness.md. - A white or black slab with a declared colour is a colour-resolution question; a flat slab over a component holder is a hosting question. They look alike but exercise different paths.
- The player's built-in
<vis>element is not the{0000000A}visualization component. The former routes torendering/vis.md; the latter routes tocomponents/visualization.md. - A
<layout>or<container>changing its own window geometry is core rendering behavior. A<Wasabi:Frame>changing the division between its children is splitter behavior. - Historical handoffs are evidence, not current routing. Resolve their old section titles through
the map, then use the focused reference and the live
TASKS.mdranking. - When two rows appear plausible, read both section headings before loading either whole file; the split is designed so the narrower file normally settles the ownership question immediately.
Where things live
All engine code is in Sources/NullPlayer/WinampModern/; all UI/controller code is in
Sources/NullPlayer/Windows/WinampModern/.
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 120
- Forks
- 9
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
winamp-modern-skin-guide- Source
- github.com/ad-repo/nullplayer