-
Notifications
You must be signed in to change notification settings - Fork 41
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.
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
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- 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
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- 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
- 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
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- 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
- 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
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- 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