-
Notifications
You must be signed in to change notification settings - Fork 41
example os app
A complete OpenStation window from one PHP file: state, actions, a
title-bar button, a ⋯-menu row, auto-refresh, and a confirm dialog —
no JavaScript. The full reference is
app-framework.md; the shipped example is
apps/code-blue/.
<?php
/**
* Plugin Name: My Desktop Extension
*/
defined( 'ABSPATH' ) || exit;
add_filter( 'openstation_apps_directories', static function ( array $dirs ) {
$dirs[] = __DIR__ . '/apps';
return $dirs;
} );<?php
use OpenStation\App;
use OpenStation\App\Os;
use OpenStation\App\State;
use function OpenStation\App\Html\attr;
use function OpenStation\App\Html\esc;
defined( 'ABSPATH' ) || exit;
return App::define( 'notes-counter' )
->title( __( 'Notes counter', 'my-plugin' ) )
->icon( 'dashicons-edit' )
->size( 480, 320 )
->capabilities( 'edit_posts' )
->desktop_icon( array( 'position' => 40 ) )
// The schema: only these keys exist, each with its type.
->state( array( 'status' => 'draft', 'auto' => false, 'note' => '' ) )
// Chrome, from PHP.
->title_bar_button( 'refresh', array(
'label' => __( 'Refresh', 'my-plugin' ),
'icon' => 'reload',
'action' => 'refresh',
) )
->window_action( 'trash-drafts', array(
'label' => __( 'Trash every draft', 'my-plugin' ),
'icon' => 'dashicons-trash',
'action' => 'trash-drafts',
'confirm' => array(
'title' => __( 'Trash every draft?', 'my-plugin' ),
'message' => __( 'They move to the Trash and can be restored for 30 days.', 'my-plugin' ),
'danger' => true,
),
) )
// Actions mutate the state; the view re-renders after each one.
// (`refresh` needs no declaration — it is the built-in that
// recomputes and re-renders.)
->action( 'trash-drafts', static function ( State $state, Os $os ) {
foreach ( get_posts( array( 'post_status' => 'draft', 'numberposts' => -1, 'fields' => 'ids' ) ) as $id ) {
wp_trash_post( $id );
}
$os->toast( __( 'Drafts trashed.', 'my-plugin' ) );
} )
// The view is a function of the state. Derived data is computed
// here, never stored in the state.
->view( static function ( State $state, Os $os ) {
$counts = wp_count_posts();
$status = $state->get( 'status' );
$total = isset( $counts->{$status} ) ? (int) $counts->{$status} : 0;
?>
<os-panel>
<os-segmented label="<?php echo esc( __( 'Status', 'my-plugin' ) ); ?>" os-bind="status" value="<?php echo esc( $status ); ?>">
<os-segment value="draft"><?php echo esc( __( 'Drafts', 'my-plugin' ) ); ?></os-segment>
<os-segment value="publish"><?php echo esc( __( 'Published', 'my-plugin' ) ); ?></os-segment>
</os-segmented>
<os-display size="xl" value="<?php echo esc( number_format( $total ) ); ?>"></os-display>
<os-text-field label="<?php echo esc( __( 'Note to self', 'my-plugin' ) ); ?>" os-bind="note" value="<?php echo esc( $state->get( 'note' ) ); ?>"></os-text-field>
<?php if ( '' !== $state->get( 'note' ) ) : ?>
<os-notice tone="info" not-dismissible><?php echo esc( $state->get( 'note' ) ); ?></os-notice>
<?php endif; ?>
<os-cluster justify="space-between">
<os-switch label="<?php echo esc( __( 'Auto-refresh', 'my-plugin' ) ); ?>" os-bind="auto"<?php echo attr( array( 'checked' => $state->get( 'auto' ) ) ); ?>></os-switch>
<os-button variant="danger" os-action="trash-drafts"
os-confirm="<?php echo esc( __( 'Move every draft to the Trash?', 'my-plugin' ) ); ?>" os-confirm-danger>
<?php echo esc( __( 'Trash drafts', 'my-plugin' ) ); ?>
</os-button>
</os-cluster>
<?php if ( $state->get( 'auto' ) ) : ?>
<span os-poll="15000" os-action="refresh" hidden></span>
<?php endif; ?>
</os-panel>
<?php
} );What each line buys you:
-
os-bind="status"on the segmented control writes the pick into the state and re-renders — the count changes with no handler at all. -
os-bind="note"on the text field debounces typing (250 ms) and re-renders; the notice appears as you type. -
os-confirmon the button asks throughwp.os.confirmbefore the action runs; the ⋯ row does the same through itsconfirmarray. -
os-pollexists only whileautois on — render it conditionally and the runtime starts and stops the timer for you. -
$os->toast()is an effect: the shell shows it after the body has morphed.
A stylesheet named after the app id, beside the .os.php, is picked up automatically and injected on the window's first open.
.os-app[data-os-app='notes-counter'] os-display {
color: var( --os-ui-accent, #2271b1 );
}// Bump the window from another bundle, e.g. after a REST mutation.
wp.os.apps.dispatch( 'notes-counter', 'refresh' );If the window re-slices rows it already has, keep the body in the browser: move the rows into ->data() on the PHP side and add a client view. os-bind writes and local actions never make a request; refresh and trash-drafts still do.
import { defineApp, html, __ } from '@openstation/app';
interface State extends Record< string, unknown > { status: string; note: string; auto: boolean }
interface Data { counts: Record< string, number > }
export default defineApp< State, Data >( 'notes-counter', {
view: ( { state, data } ) => html`
<os-panel>
<os-segmented label=${ __( 'Status' ) } os-bind="status" value=${ state.status }>
<os-segment value="draft">${ __( 'Drafts' ) }</os-segment>
<os-segment value="publish">${ __( 'Published' ) }</os-segment>
</os-segmented>
<os-display size="xl" value=${ String( data.counts[ state.status ] ?? 0 ) }></os-display>
<os-button os-action="refresh">${ __( 'Refresh' ) }</os-button>
${ state.auto ? html`<span os-poll="15000" os-action="refresh" hidden></span>` : '' }
</os-panel>
`,
} );->data( static function () {
return array( 'counts' => (array) wp_count_posts() );
} )npm run build:apps builds it; the host loads it with the window. See app-framework.md → The client view.
The same file runs on a bare PHP host through the standalone adapters —
see Standalone host in three lines.
Only the two get_posts() / wp_count_posts() calls above are
WordPress-specific; an app that talks to the host through $os alone
needs no changes.
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
- 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: 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
- 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
- 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