Winamp Modern (.wal) Skin Engine

SkillCloud & infra

Winamp 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.

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=1 says 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=1 is 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 withNot
Debugging one named skinskins.mdskins/<skin>.md, then reference/harness.mdreading 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 axisad-hoc dumps whose findings evaporate
Deciding what to implement next across many skinstriage-playbook.md — corpus measurement, defect classes, the demand indexfixing 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 / questionRead
Load, mounts, @VARS@, include/glob, sibling skinsreference/loading.md
Geometry, anchors, y-origin, collapsed windowsreference/loading.md
The bundled default skin, or Modern mode has nothing to loadreference/loading.mdThe bundled default skin
A window opens at its minimum_* rather than the size its skin declaresreference/loading.mddefault_w/default_h are <container> attributes too
A component window (visualizer, video) opens as a sliverreference/loading.mdA component window with no stated size
Skin starts in an impossible all-zero settings statereference/loading.mdsettings must start in a state scripts can express
Where a skin's windows open, overlapping windows, the tilingreference/components.md
Skin-opened window appears in the wrong placereference/components.mddefault_visible
Several windows in the menu share one namereference/components.mddefault_visible, and WinampModernContainerTopology.menuLabels
A window the skin closes at startup is missing from every probereference/components.mdvisible on a container answers two questions
Window restores at the wrong size or one skin inherits another's framereference/rendering.mdA .wal window's size is still the skin's
Container/layout writes do not move or size their windowreference/rendering.mdA container's x/y/w/h are its window's
A bare outline rectangle opens beside the player (drop shadow, snap preview)reference/components.mdA window that only fakes a Windows desktop effect

Drawing: sprites, colour, text

Symptom / questionRead
cfgattrib, onActivate, alpha, fill, sliders, ProgressGrid, animation; album art, animated layer or image parameter draws stale/wrongreference/rendering.md
Toggle works but never looks activereference/rendering.mdonActivate
Playlist/EQ/library lamp on a skin button is inverted or counts clicksreference/rendering.mdA TOGGLE button's lamp
Shuffle/repeat/crossfade disagree with the hostreference/rendering.mdSome cfgattrib values are the host's
Vertical slider uses the wrong axis or EQ curve is absentreference/rendering.mdA skin spells the axis two ways
Bitmap icons/borders look blurry at an integer UI Sizereference/rendering.mdBitmap interpolation follows UI Size × backing scale
A window has no border or background, or a skin's own labels are invisible against itreference/rendering.mdA standard frame whose artwork stayed in Winamp
Settings window is an empty slab, or a <Wasabi:TitleBox> shows neither label nor bodyreference/rendering.md<Wasabi:TitleBox> is a body, not just a border
Settings page has boxes but no switches, labels, sliders or drop-downsreference/rendering.mdThe 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 appearsreference/rendering.mdA skin's own right-click menus
Colour resolution, themes, unreadable selections/titlesreference/rendering/colour.md
White/black slab appears where a named colour belongsreference/rendering/colour.mdHow a colour resolves
Selected row or title text matches its backgroundreference/rendering/colour.mdA resolved colour is not yet readable
A title draws as a smear, or two objects in one slot both draw, or artwork ignores window focusreference/rendering/colour.mdactivealpha/inactivealpha
A <gradient> fills flat instead of rampingreference/rendering/colour.mda <gradient> with no direction
The playing playlist row has no marker, or the selection bar never appearsreference/rendering/colour.mdA marker only marks when it differs
Theme picker is empty or will not switchreference/rendering/colour.mdThe picker
A skin's colours are readable-but-bad and the user wants to fix them by handreference/rendering/colour.mdA user override outranks the chain
Text metrics, fonts, clocks, bitmap fonts, missing height, offsetsreference/rendering/text.md
A paragraph draws as one clipped line, or wrap=/<Wasabi:Text> layoutreference/rendering/text.mdA paragraph is not a line
Playlist/library/tab text draws in a console font, or a size too smallreference/rendering/text.mdHost-drawn text is not skin-declared text
Clock fields collide or the separator sits off baselinereference/rendering/text.mdA clock is a run of fields
A separator or icon a script placed sits on the string beside itreference/rendering/text.mdgetTextWidth() carries the box's own margin
A readout's glyphs are chopped at the bottom, or sit low in their boxreference/rendering/text.mdA 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 stormsreference/performance.md

Input and hit testing

Symptom / questionRead
Dead mouse target, clipping, regions, drag policy, sysregionreference/rendering/hit-testing.md
Invisible layer blocks clicks or drags the windowreference/rendering/hit-testing.mdHit testing
A control is dead because a second object shares its rect, or a skin declares the same id twicereference/scripting.mdgetObject 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 termsreference/scripting.mdembed_xui — the wrapper is the control
Control works once, hides itself, and cannot be clicked againreference/scripting.mdA layout must not be left with no way to seek
Config/EQ drawer or custom list will not scrollreference/scripting.mdThe mouse wheel is a layout event

Scripting (MAKI)

Symptom / questionRead
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 driftreference/scripting.md
A call trace stops mid-handler with no failure linereference/scripting.mdAn 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 contentreference/scripting.mdWriting back the position a window just read
A window a skin's own script closed never comes back, or opens emptyreference/scripting.mdonSetVisible — a window a script closes has to be reopened
One window's script sets something in another window and nothing happensreference/scripting.mdgetLayout() answers NULL for a layout that has never been shown
Dragging one window should pull another along and does notreference/scripting.mdonMove() is dispatched to the window objects only

Components and hosted surfaces

Symptom / questionRead
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 teardownreference/components.md
hold="none", flat holder slab, component routingreference/components.mdComponent hosting
A window opens as the skin's own frame around an empty holereference/components.mdA frame the skin drew and left for Winamp to fill
Component bucket/thinger, or a skin declares a widget that never enters its include closurereference/components.mdThe component bucket
Hosted surface survives the wrong tab or remounts deadreference/components.mdUnmounting is not teardown
Mode switch teardown crashes or leaks a hosted surfacereference/components.mdTeardown order
Embedded playlist/library text sizes disagreereference/components.mdHow large NullPlayer draws its own text
A skin's own About page, skin.about.group, the About GUIDreference/components.mdThe About page is a group, not a window

Visualization, video, browser, notifier

Symptom / questionRead
<vis> analyzer/oscilloscope, gain, colours, modesreference/rendering/vis.md
Visualization timing, stepped motion, pause freezereference/performance.mdThe visualization has a clock of its own
AVS/MilkDrop component holder or embedded visualizationreference/components/visualization.md
Plugin pane draws an analyzer and the user wants something else in itreference/components/visualization.mdAn unhosted pane is a surface with a choice of its own
Several visualization holders show the wrong enginereference/components/visualization.mdone holder per skin
Video picture, child-window sizing, control barreference/components/video.md
Clock or transport frozen while a video playsreference/components/video.mdThe picture's clock
Video window pops out when a tab changesreference/components/video.mdA holder leaving is a tab switch
Browser/WebKit, navigation, search URL, duplicate toolbarreference/components/browser.md
Scheme-less web address is mistaken for a VFS pathreference/components/browser.mdfour navigation routes
Notifier toast text, layout, visibility, timingreference/components/notifier.md
Notifier title is invisible or rows overlapreference/components/notifier.mdtext and layout

The compatibility surface, and the other engines

Symptom / questionRead
Supported Wasabi markup; slider action families and onSetPosition; playlist/visualization/video toolbar actionscompatibility/wasabi-surface.md
Host action is accepted but deliberately inertcompatibility/wasabi-surface.md → action families
Implemented MAKI method/eventscompatibility/maki-surface.md
Security model, limits, policy, verification statuscompatibility/limits-and-policy.md
ClassicPro engine/import behaviorreference/classicpro.md
WACUP probe, branding branch, WACUP-only surfacereference/wacup.md

Instruments, proof, and what to work on

Symptom / questionRead
Probes, env vars, dumps, live defectreference/harness.md
Probe reports no match or no eventreference/harness.mdA blind instrument reads as a working feature
Whole skin dead or startup handler abortsreference/harness.mdThe order that made Phase 33 cheap
Meter moves too littlereference/harness.md → histogram the frames
GUI-only scripted-control reportreference/harness.mdAsk for the live trace first, not fourth
Frame is fast but the app hangsreference/harness.mdProfiling the running app
Renderer regression proofreference/harness.mdThe golden images
Proving an engine-wide change broke no other skinreference/harness.mdThe corpus render sweep
Measure one skin end to end/wal-skin-report <skin.wal>
The GUI verification pass before handing work overmanual-qa-checklist.md
One named skin's current stateskins.mdskins/<skin>.md
Choose the next cross-skin capabilitytriage-playbook.md, then the ranked Reach table in TASKS.md
Current open workTASKS.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.md owns drawable behavior that does not belong to hit testing, visualization, splitters, text, or colour; the parent components.md owns 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 to rendering/vis.md; the latter routes to components/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.md ranking.
  • 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