-
Notifications
You must be signed in to change notification settings - Fork 42
example register wallpaper
The OpenStation Preferences wallpaper picker is registry-driven: every entry in the registry becomes a swatch users can select. Register your own via wp.os.registerWallpaper() from inside a os.init action so the public API is guaranteed available.
Two types today: CSS (a static background value) and canvas (a plugin-managed DOM subtree, typically a WebGL/2D canvas).
The smallest possible registration. A one-liner value + a matching preview used as the swatch.
my-plugin.php
<?php
/** Plugin Name: My Wallpaper */
defined( 'ABSPATH' ) || exit;
add_action( 'admin_enqueue_scripts', function () {
wp_enqueue_script(
'my-wallpaper',
plugins_url( 'my-wallpaper.js', __FILE__ ),
array( 'openstation' ), // <- hooks into the shell
'1.0.0',
true
);
} );my-wallpaper.js
wp.os.ready( () => {
wp.os.registerWallpaper( {
id: 'my-plugin/ocean',
label: 'Ocean',
type: 'css',
value: 'linear-gradient(180deg, #0ea5e9, #1e3a8a)',
preview: 'linear-gradient(180deg, #0ea5e9, #1e3a8a)',
description: 'Sea-surface blues fading into deep water.',
} );
} );The swatch appears in OpenStation Preferences next time the panel opens. Clicking it writes the value to --os-bg and persists the user's selection to localStorage.
description (optional) is a sentence or two shown in a styled card under the picker grid while your wallpaper is the active selection — tell the user what they're looking at. Plain text only. When registering server-side, pass it to openstation_register_wallpaper() (translatable with __()); the shell overlays it onto your JS def automatically.
Desktop icons, their captions and the desk's file tiles paint straight onto your wallpaper, so the shell picks their ink from one field:
wp.os.registerWallpaper( {
id: 'my-plugin/linen',
label: 'Linen',
type: 'css',
value: 'linear-gradient(180deg, #fdfbf7, #efe7db)',
preview: 'linear-gradient(180deg, #fdfbf7, #efe7db)',
tone: 'light',
} );tone: 'light' switches the desk from Starlight to Void: the captions, the file tiles, and the icon artwork too, since silhouette SVGs are masked with currentColor.
Leave it off if you are unsure. Unset means 'dark', and the direction that hurts is a wrong 'light': Void icons on a Void sky. If the brightness is the user's choice rather than yours (they pick the colours, or drop in a photograph) leave it unset and the shell measures the finished surface instead.
Declare dependencies by module id — the shell ships pixijs pre-registered and loads it before mount fires the first time anyone activates a wallpaper that needs it. Concurrent activations dedupe the fetch automatically.
wp.os.ready( () => {
wp.os.registerWallpaper( {
id: 'my-plugin/particles',
label: 'Particles',
type: 'canvas',
preview: '#0a0a1a',
needs: [ 'pixijs' ], // ← that's it
mount: async ( container, ctx ) => {
// window.PIXI is guaranteed available at this point.
const app = new window.PIXI.Application();
await app.init( { resizeTo: container } );
container.appendChild( app.canvas );
// Reduced-motion: render a still frame and bail.
if ( ctx.prefersReducedMotion ) {
app.ticker.stop();
drawStillFrame( app );
return () => app.destroy( { removeView: true } );
}
// Pause on tab-hidden via the shell's visibility action.
const onVisibility = ( detail ) => {
if ( detail?.id !== 'my-plugin/particles' ) return;
if ( detail.state === 'hidden' ) app.ticker.stop();
else app.ticker.start();
};
wp.hooks.addAction(
'os.wallpaper.visibility',
'my-plugin/particles-visibility',
onVisibility
);
// Teardown — MUST release GL/animation resources or
// switching wallpapers leaks memory.
return () => {
wp.hooks.removeAction(
'os.wallpaper.visibility',
'my-plugin/particles-visibility'
);
app.destroy( { removeView: true } );
};
},
} );
} );Never call
app.destroy( true ). In PixiJS v8 a literaltrueas the first argument runsreleaseGlobalResources(), which clears Pixi's page-global texture and object pools — corrupting every other live Application on the page (the OpenStation Preferences live previews, other canvas wallpapers, any plugin's Pixi window). Symptoms are crash loops inBatcher.break()and teardown throws inTexturePool.returnTexture(). Useapp.destroy( { removeView: true } )— same canvas cleanup, no global wipe.
If you use a library that isn't pre-registered, register it once. Other plugins can then needs: it by id and share your fetch.
wp.os.ready( () => {
wp.os.registerModule( {
id: 'three-js',
url: `${ wp.os.config.pluginUrl }/vendor/three.min.js`,
isReady: () => typeof window.THREE !== 'undefined',
} );
} );
// ...elsewhere (same plugin or another):
wp.os.registerWallpaper( {
id: 'my-plugin/starfield',
type: 'canvas',
needs: [ 'three-js' ],
mount: /* ... */,
} );Any wallpaper can ship renderEditor. When that wallpaper is the selected swatch in OpenStation Preferences, a collapsible panel opens below the grid and your editor is rendered into it — same animation as the built-in custom-gradient editor.
const state = { tint: '#6366f1' };
wp.os.ready( () => {
wp.os.registerWallpaper( {
id: 'my-plugin/tintable',
label: 'Tintable',
type: 'css',
preview: state.tint,
resolveValue: () => state.tint, // re-read on every apply
renderEditor: ( container ) => {
const input = document.createElement( 'input' );
input.type = 'color';
input.value = state.tint;
input.addEventListener( 'input', () => {
state.tint = input.value;
// Force an apply so the layer re-reads resolveValue.
// (A helper for this pattern may ship in a future release.)
wp.os.registerWallpaper( {
id: 'my-plugin/tintable',
label: 'Tintable',
type: 'css',
preview: state.tint,
resolveValue: () => state.tint,
renderEditor: /* reference same function */ undefined,
} );
} );
container.appendChild( input );
return () => input.remove();
},
} );
} );A canvas wallpaper's preview string is a static stand-in. Ship renderPreview and the OpenStation Preferences picker mounts the real thing (or a cheap facsimile) inside the swatch tile — lazily, only while the tile is visible, capped at 4 concurrent previews page-wide, with the CSS preview as the fallback for every failure mode.
ctx.params parametrizes what the preview depicts: it's your def's previewParams after the os.wallpaper.preview-params filter. Use it when the honest render would look wrong in a thumbnail — the built-in Living Tree previews a 540-day-old showcase site so a day-old install doesn't advertise the wallpaper as a bare sprout.
wp.os.ready( () => {
wp.os.registerWallpaper( {
id: 'my-plugin/aquarium',
label: 'Aquarium',
type: 'canvas',
preview: '#04263b', // instant paint + fallback
needs: [ 'pixijs' ], // loaded before renderPreview too
previewParams: { fishCount: 12 }, // idealized for the tile
mount: async ( container, ctx ) => { /* the real thing */ },
renderPreview: async ( container, ctx ) => {
const app = new window.PIXI.Application();
await app.init( { resizeTo: container, resolution: 1 } );
container.appendChild( app.canvas );
swim( app, Number( ctx.params.fishCount ) || 12 );
if ( ctx.prefersReducedMotion ) {
app.render(); // one still frame, no ticker
app.ticker.stop();
}
return () => app.destroy( { removeView: true } );
},
} );
} );Site owners and plugins can re-parametrize any wallpaper's preview without touching its code:
wp.hooks.addFilter(
'os.wallpaper.preview-params',
'my-plugin/more-fish',
( params, wallpaperId ) =>
wallpaperId === 'my-plugin/aquarium'
? { ...params, fishCount: 40 }
: params
);renderEditor (Recipe 3) is an inline panel and owns its own state. For a fuller settings form with persistence for free, ship renderConfig instead: OpenStation Preferences shows a "Wallpaper settings" button for your wallpaper (only when selected, only because you opted in), clicking it opens a <os-modal> with your form inside, and ctx.setSettings() saves through the user's OpenStation Preferences (localStorage + user meta — values follow the user across devices).
Every wallpaper context (mount, renderPreview, renderEditor, renderConfig) reads the persisted bag back as ctx.settings. Each setSettings call also fires the os.wallpaper.settings-changed action with the full post-merge bag, so a mounted wallpaper applies edits live — the dialog doubles as a tuning panel.
window.openStationWallpapers = window.openStationWallpapers || {};
window.openStationWallpapers[ 'my-plugin/aquarium' ] = {
id: 'my-plugin/aquarium',
label: 'Aquarium',
type: 'canvas',
preview: '#04263b',
needs: [ 'pixijs' ],
mount: async ( container, ctx ) => {
// Untrusted read-back: clamp to your defaults.
const scene = await swim( container, Number( ctx.settings.fishCount ) || 12 );
const onSettings = ( detail ) => {
if ( detail?.id !== 'my-plugin/aquarium' ) {
return;
}
scene.setFishCount( Number( detail.settings.fishCount ) || 12 );
};
wp.hooks.addAction(
'os.wallpaper.settings-changed',
'my-plugin/aquarium-live',
onSettings
);
return () => {
wp.hooks.removeAction(
'os.wallpaper.settings-changed',
'my-plugin/aquarium-live'
);
scene.destroy();
};
},
renderConfig: ( container, ctx ) => {
const field = document.createElement( 'os-range-field' );
field.setAttribute( 'label', 'Fish' );
field.setAttribute( 'min', '1' );
field.setAttribute( 'max', '60' );
field.setAttribute( 'value', String( Number( ctx.settings.fishCount ) || 12 ) );
field.addEventListener( 'os-range-change', ( e ) => {
ctx.setSettings( { fishCount: e.detail.value } ); // persists + fires the action
} );
container.appendChild( field );
return () => {};
},
};Scalar values only (string | number | boolean) — the server-side sanitizer drops anything else, and caps the bag at 32 keys (strings at 256 chars). The built-in Snow wallpaper (wp-snow, src/plugins/snow-wallpaper/) is the in-tree reference: wind, snowflake count, flake size, and backdrop colour, all live-applied.
The os.wallpapers filter receives the full list — add, remove, or reorder in one shot.
// Hide the stock 'aurora' preset.
wp.hooks.addFilter(
'os.wallpapers',
'my-plugin/hide-aurora',
( list ) => list.filter( ( w ) => w.id !== 'aurora' )
);A canvas wallpaper doesn't have to be self-contained — it can pull site data over REST at mount time and shape itself from it. The built-in Living Tree wallpaper (wp-living-tree) is the reference for this pattern: on mount it fetches desktop-mode/v1/living-tree/snapshot through wp.os.fetch (so the request feeds the activity bus), turns the compact site "DNA" into normalised parameters, and renders a growing tree with PixiJS. Its algorithm is fully specified in ../living-tree-algorithm.md, and the source under src/plugins/living-tree-wallpaper/ is a good skeleton to copy: index.ts (fetch + publish the def), scene.ts (PixiJS app, layers, ticker, teardown), plus a narrow pixi-types.ts so the bundle never imports pixi.js directly.
The one rule worth stealing: fetch through the framework, never raw fetch() — use trackedFetch (in-bundle) or window.wp.os.fetch (external), with { silent: true } for a background pull the user didn't initiate.
-
Hooks catalog — every
os.*hook with its payload shape. -
Wallpaper registration API — full
WallpaperDeftype, includingrenderPreview/previewParams/renderConfig. - The Living Tree — algorithm definition — a worked canvas-wallpaper spec that consumes REST site data.
This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- The App Framework — a window in one PHP file
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Data model — where OpenStation keeps its data
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Window-scoped MIO
- Mio
- Mobile — the phone layer
- Multisite
- Native Windows & Framework Interop
- OpenStation Network
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
- Workspaces
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration — Code Blue becomes an App Framework app
- Migration — AI comment scoring leaves core
- Migration: window, wallpaper and widget bundles load on demand
- Migration — Posts, Pages, Users, User Edit, Plugins and Comments become App Framework apps
- Migration — the navigation model
- Migration — OpenStation Preferences becomes an App Framework app
- Performance settings move to Extended options
- Presence storage migration
- Migration — the Recycle Bin becomes an App Framework app
- Migration — the shell boots from its own screen
- Migration — Station Home becomes an App Framework app
- Migration: a native window's tabs move to the window chrome
- Migration — WP Explorer becomes the my-wordpress app
- Migration — WordPress package globals are no longer ambient
More
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- App layout recipes
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Edit a record with one form values map
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Repairable form edits with MIO
- Register a window companion
- Pin your app to the phone tab bar, and react to the mode
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- WP Explorer — add a column to the list view
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- Ship a window as an .os.php app
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Revisions in their own window — extend or redirect "View revisions"
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Observe stored-file cleanup failures
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config
- Place something where the user can reach it — wp.os.workArea
- Ship a workspace template