How it works
Onbun is two programs. On your computer, a small companion owns everything: it plays your music, holds your logins, talks to Discord and Windows audio, and runs the apps. On your iPhone or Android phone, a native app turns sideways into a control surface for it.
The phone never runs an app's logic. Each app sends the phone a screen described in JSON, the phone draws it with native components, and taps come back to the PC as commands: {module, action, value}. That is why installing or updating an app never needs a new phone build, and why every app works the same on iPhone and Android.
The PC does the work and the phone shows it. Close the phone app and the music keeps playing on the PC.
Install
Onbun runs on Windows, macOS, and Linux. Choose your desktop download from the download section. Onbun was called WinMore before 0.9.0; connection identifiers and the .wmapp package format keep that name so older installs keep working.
- Windows: run
Onbun-Setup-<version>.exe. It installs for your user (no admin needed), adds desktop and Start menu shortcuts, and starts. The installer is not signed yet, so SmartScreen may ask you to confirm. Allow Onbun through the firewall on private networks when Windows asks: the phone reaches it over your network. - macOS: open the
.dmg(arm64for Apple silicon,x64for Intel) and drag Onbun to Applications. It isn't notarized yet: the first time, right-click it and choose Open. Onbun lives in the menu bar, with no Dock icon. - Linux: use the
.AppImage(make it executable and run it; this is the one that updates itself) or install the.deb. On Wayland desktops the island runs through XWayland so it can stay on top and follow the pointer.
The first time Onbun starts, a cartoon arm pulls the island down from the top of your screen and welcomes you. Until a phone is paired, the resting island says Open Onbun to start pairing. Turn on Open at sign-in in the island's Settings → General to start it with your computer.
Uninstall it like any other app (on Windows, from Settings → Apps).
Pair your phone
- Install the phone app: on Android, download
Onbun-Android-<version>.apkfrom the download section on the phone and open it (Android asks once to allow installing from your browser). On iPhone, install the IPA when a release has one (it's an Ad Hoc build, so your iPhone has to be registered for it), or build the app from source with Xcode. - Put the phone and the PC on the same trusted Wi-Fi or LAN, and open Onbun on the phone.
- The first launch plays a short welcome on a black screen: turn the phone sideways, with its top on the left, and cartoon arms set out the app's navigation while Settings walks in on its own legs.
- Connect to your PC then lists the PCs running Onbun nearby. Allow local network access when the phone asks, and choose yours.
- The island opens with a six-digit code, shown as
123 456. Type it on the phone within two minutes (five tries). Decline on the island if you didn't ask for it. - That's it: the app slides in from the edges of the screen, and from then on the phone remembers the PC and reconnects by itself, even when its address changes.
If the phone can't find the PC (some networks block discovery), tap Use a pairing link instead and use the link from the island's Settings → General → Pairing link. Keep that link private. To add another phone later, choose Connect in the same place.
To unpair everything, use Forget phones in Settings → General: the PC gets a new key and every phone has to pair again.
The island
The island is Onbun's face on the PC: always on top, OLED black, and out of the way until you need it. The menu at the top of this site is a copy of it.
- Move it: drag the pill (or the expanded header). It snaps to one of eight docks, the corners and edge midpoints, on any monitor, and remembers where you left it.
- Hide it: the × button dismisses it while music and the phone keep working. Bring it back with
Ctrl+Alt+Wor the tray icon. - Type from the PC: every text field on the phone has a keyboard icon. Tap it and the island opens Typing to phone: what you type on the PC appears in the phone field live.
Entersubmits,Escstops. - Apps on the island: apps can briefly take over the island, like Clipboard showing something sent from the phone with Copy and Dismiss. It goes back to your music after 30 seconds.
- Notifications: when an app has something new for you, a dot appears on the island and on the app's icon on the phone. Open the island to read the list and clear it; opening the app on the phone marks them seen.
- Install prompts: when a website's Add to Onbun button asks for an app, the island turns into a small prompt (Add Weather, with Install and Cancel). Nothing installs until you choose Install. Apps from outside the Onbun catalog get a warning that names where they come from.
- Settings: the gear opens General (start at sign-in, connect a phone, pairing link, forget phones, Onbun updates), Apps (what's installed, app updates, the apps folder), Library (get apps), and a page for each app that has its own settings.
The phone app
A native app with a rail of your apps on the side, built for landscape (portrait works too). It keeps the screen awake while open.
- iPhone: SwiftUI. On iOS 26 and later its floating controls use Liquid Glass; content sits on flat system surfaces.
- Android: Jetpack Compose with Material 3 Expressive: a floating toolbar, tonal buttons and Material's own sliders, switches and sheets, colored from each app's accent. Android 10 or later.
Both draw the same screens: an app is written once, and each phone gives it its own native look.
Pick which apps sit in the rail in the phone's Settings → Navigation. Every screen in the app, the built-in ones included, is drawn from the JSON its app sends, so new apps show up on the phone as soon as they're installed on the PC.
- Settings is a page like the others: it opens in place of the current app, with the rail still beside it.
- Apps stay where you left them: switch apps and come back, and each one is still scrolled to the same place.
- Dots: an app with unseen notifications shows a dot on its icon, and Settings shows one when an update is waiting.
Get apps
Everything Onbun does comes from apps. Find them in the island's Settings → Library:
- Onbun app store: Open store shows every app on this site. Each app's page has an Add to Onbun button; it hands the app to Onbun on your computer, and the island asks before installing.
- Quick install: bundles add several apps at once: Audio essentials (Music, Mixer, Deezer), Desktop control (PC, Clipboard, Browser) and Stay connected (Discord, Reddit, Twitch). More apps lists the rest.
- For developers: install an app folder, a
.wmappfile, or the newest release from a GitHub repository (owner/app). An app from GitHub keeps updating from that repository.
Apps run on your PC with Onbun's access to it. The catalog is reviewed; for anything else, install only apps you trust. Remove an app from Settings → Apps.
Updates
- Onbun on your computer checks a few seconds after launch and every six hours. When there's a new release, the island shows Update to <version>: one click downloads it, installs it and restarts. This works on Windows, on macOS, and on Linux with the AppImage (a
.debinstall updates through a new.deb). Settings → General → Check now checks right away. - Apps update themselves (Settings → Apps → Update apps automatically), or with Update, Update all and Check now. Catalog apps update from the catalog. Apps installed from GitHub or a link update from where they came from, and say so under their name.
- The Android app shows Update to <version> in Settings when a new build exists; it downloads the APK and Android installs it over the current one.
- The iPhone app shows a blue dot on Settings when a new build exists. Ad Hoc builds install in place; builds from Xcode are reinstalled from Xcode.
- Updating to 0.9.1 means updating every copy of Onbun, the PC and each phone, because of the encrypted connection. Apps don't need an update, and paired phones stay paired.
Network and privacy
- Everything stays on your network. The PC listens on port
4317and announces itself over Bonjour as_winmore._tcp. - The connection is encrypted (since 0.9.1). Everything between your phone and your PC is encrypted and authenticated: app data, commands, what you type, clipboard and photos, package files and videos. Someone else on the same Wi-Fi can't read or change it, and a device pretending to be your PC can't answer as it.
- Pairing never sends the code. The six-digit code on the PC stays on your two screens: your phone uses it to derive a key, and only a phone that knows the code can open the PC's answer. Each code allows five tries and expires after two minutes.
- Every connection uses fresh keys mixed with the pairing key, so recorded traffic stays unreadable even if that key leaks later. The PC refuses unencrypted requests from other devices, so phones and PCs older than 0.9.1 can't connect to newer ones: update both.
- Forget phones (Settings → General on the island) replaces the PC's key: every phone has to pair again. Anyone holding the pairing link can still control the PC, so keep it to yourself.
- The pairing key is random and stored encrypted with your account (Windows' data protection, the macOS Keychain, or the Linux keyring). Logins (YouTube Music, Reddit) live in Onbun's own browser profile on the PC; secrets like Discord's never reach the phone.
- Apps run inside Onbun with its access to your PC. The catalog is published only by its owner; for anything from elsewhere, Onbun asks before installing.
- Add to Onbun buttons only reach Onbun on the same computer: the website sends the link to
127.0.0.1, or opens anonbun://link, and the island always asks first.
The catalog
Every feature in Onbun is an app, installed from Settings → Apps on the island. Here's what each one does and what it needs.
Build an app
An app is a folder. It runs on the PC; its phone screen is JSON each phone draws natively. Copy this, change the names, and you have an app.
The same guide is available as plain files, for reading offline or handing to a coding agent: the app guide, the component reference, and the JSON Schema for ui/screen.json. The catalog apps' full source is public too, at raw.githubusercontent.com/MathieuDvv/WinMore-releases/main/apps/<id>/.
Prompt for coding agentsPaste it into Claude Code, Codex or any agent, then describe your app on the first line.+
Build an Onbun app: <DESCRIBE THE APP: what it shows, what it controls, which service or PC feature it uses>.
Onbun runs apps on the user's computer (Windows, macOS, Linux). Each app's phone screen is a JSON file that the
iPhone app (SwiftUI, Liquid Glass) and the Android app (Compose, Material 3 Expressive) draw with native
components. Taps come back to the computer as commands. Make the app complete and polished, like the apps in
the Onbun catalog.
READ FIRST. These are the source of truth. Never guess a component, prop, action or operator.
- Guide: https://onbun.app/docs
- Full app guide: https://onbun.app/data/app-guide.md
- Every component, prop, action, operator and filter: https://onbun.app/data/components.md
- JSON Schema for ui/screen.json: https://onbun.app/data/screen.schema.json
- Real apps to learn from (read a few before writing anything):
https://raw.githubusercontent.com/MathieuDvv/WinMore-releases/main/apps/<id>/ui/screen.json
https://raw.githubusercontent.com/MathieuDvv/WinMore-releases/main/apps/<id>/index.cjs
https://raw.githubusercontent.com/MathieuDvv/WinMore-releases/main/apps/<id>/manifest.json
Good ids: weather (backdrop, sheets, menus, landscape/portrait split), agents (backdrop, cards, sheet),
reddit (feed, paging cards, covers, optimistic votes), music (media, timelines, island), discord, clock, mixer.
Every file of every app is listed in https://raw.githubusercontent.com/MathieuDvv/WinMore-releases/main/index.json
DELIVER a folder named exactly like the app id:
manifest.json id, name, shortName, version, description, entry "index.cjs", ui "kit",
screen "ui/screen.json", symbol (an SF Symbol), icon "icon.svg"
index.cjs activate(ctx) returns { data(context), command(action, value), close() }
lib/ other backend code, if any
ui/screen.json the phone screen
assets/ images the screen uses (assets/backdrop.png, …)
icon.svg a single black glyph on a transparent background
ARCHITECTURE
1. All logic runs on the computer in index.cjs: network calls, files, logins, secrets, timers. Store files in
ctx.dataDir. The phone never runs your code and never sees secrets.
2. data() returns plain JSON that the screen binds to. It runs about every 1.5 s while the app is open, so it
must be cheap: fetch from services in the background (timers, with backoff), cache, and return the cache.
Add a "loading" flag while work is running and a "revision" for large data.
3. command() answers quickly. On failure, throw new Error("A readable sentence") and the phone shows it.
4. What only the phone cares about (is a search open, a field's text, a pending vote) goes in the screen's
"state" ($ui.name, changed with the set / toggle actions), not in the backend.
5. Write the screen once for every phone. Never read $env.platform to change the layout, and never add
iPhone-only or Android-only props or workarounds. Each phone draws its own native version of each component.
COMPONENTS FIRST
6. Build only from Kit components, and check every prop in components.md. Unknown props are errors.
Layout: stack layers split scroll grid list section card each if pager spacer divider
Content: text label symbol image gallery video media badge stat progress gauge chart avatar row comment
empty lyrics clock web
Controls: button pill menu toggle slider scrubber stepper picker field link
7. Use the component made for the job instead of rebuilding it from stacks: row for list rows, stat, gauge and
chart for numbers, media, gallery and video for media, scrubber and lyrics with screen "timelines" for
playback, empty for empty states, picker, menu, toggle and stepper for choices, field for text input.
8. "web" is a last resort for a visual no component can draw. Never use it for whole screens, lists, buttons
or forms.
9. Symbols are SF Symbol names (Android maps them to Material Symbols). Prefer common ones.
LAYOUT TRAPS (the usual mistakes)
10. Full-app background: set the screen-level "background" (a color) and, for an image or glow, the
screen-level "backdrop" ("assets/backdrop.png" or a binding like "$artwork"). The phone draws it behind
the whole app, under the bar and to every edge. Do NOT draw the app background inside "body" (an image
as the first child of "layers", or a stack with a background or gradient): the body starts below the
floating bar, so it leaves a band at the top and never reaches the edges.
11. Use "chrome": "fullscreen" for a designed app. It gets the whole content area down to the bottom edge;
a scroll keeps the bottom margin as space after its last item.
12. The bar floats over the content. Only a body whose top-level type is "scroll" or "list" starts below the
bar and scrolls beneath it. Any other body (stack, split, if, layers) is placed below the bar. Don't add
your own top padding to clear the bar, and don't wrap the main scroll in another container.
13. height: "screen" is the whole content height, including the strip behind the bar. Subtract the bar and
your spacing with "screenOffset" (Reddit's paging cards use 106) so nothing hides under it.
14. Glass ("glass": true, "variant": "glass") is only for floating controls in the bar and over media.
Content sits on flat "card" and "section" surfaces, colored by the screen's "surface". Set "accent".
15. The phone is sideways (landscape, with a navigation rail on the left), but portrait must work too. Use
"$env.landscape", "split" with "portrait": "stack", "grid", "fill" and "maxWidth". Never use fixed widths
that can overflow.
16. Use text "style" (largeTitle, title2, headline, body, caption…) and the semantic colors primary,
secondary and tertiary rather than fixed sizes and hex colors everywhere.
17. Put settings and forms in "sheets" (open with {"sheet": "name"}) and detail pages in "covers"
({"cover": "name", "params": item}). Give each cover a bar with a glass back button ({"dismiss": true}).
18. Instant feedback: set a $ui variable, send the command with an onError that resets it, clear it, and show
{"coalesce": ["$ui.pending", "$saved"]}.
19. Give every icon-only button an "accessibility" label. Give items in "each" a stable "key".
20. Only ui/, assets/ and the icons reach the phone. Reference images as "assets/name.png" or https URLs.
OPTIONAL
- island(): put the app on the computer's island (see the guide's "Island and PC settings" part).
- settings: { describe(), action() }: a settings page for the app in the island's settings.
- ctx.notifications.post({ title, body, id }): a dot on the app's icon and on the island.
BEFORE YOU FINISH
- Every binding in ui/screen.json exists in data() (or in $ui, $env, $params, or a let/each name).
- ui/screen.json validates against the JSON Schema. With the Onbun source, run: npm run check:apps -- path/to/app
- Empty, loading and error states all look intentional (use "empty", "progress" and readable errors).
- Bump "version" on every change.
- Tell the user to drop the folder into Onbun's apps folder (island → Settings → Apps → Open folder). It loads
at once, reloads on every save, and the phone shows exactly what to fix if the screen has a mistake.
The prompt is plain text: edit it freely. Its links point to this site and to the public catalog, so agents without access to Onbun's source can still read everything.
hello/ the folder name must equal the app id ├── manifest.json id, name, version, entry, "ui": "kit" ├── index.cjs activate(): data() and command(), runs on the PC ├── lib/… any other backend code (only the PC sees it) ├── ui/ │ ├── screen.json the phone screen, built from Kit components │ └── chart.html optional web blocks ├── assets/… images, fonts, video the screen uses └── icon.svg · icon.png the app's glyph
Only ui/, assets/ and the icons reach the phone. Add "public": ["ui", "assets", "media"] to the manifest to share other folders.
manifest.json
{
"id": "hello",
"name": "Hello",
"shortName": "Hello",
"version": "1.0.0",
"description": "What it does, in one sentence.",
"entry": "index.cjs",
"ui": "kit",
"screen": "ui/screen.json",
"symbol": "hand.wave",
"icon": "icon.svg",
"iconPNG": "icon.png"
}
| Field | What it does |
|---|---|
id | A lowercase letter, then lowercase letters, digits or dashes (40 at most). Must equal the folder name. |
version | Semver. Installed copies update when it goes up, so bump it on every release. |
ui · screen | "kit" and the path of the screen file. |
symbol | The SF Symbol the phone shows until iconPNG arrives (Android draws the matching Material Symbol). |
icon · iconPNG | A black SVG glyph (Onbun shows it grey) and the white 120 px PNG made from it with npm run render:icons. |
platforms | Any of "win32", "darwin", "linux". Leave it out for all three. The app can't be installed on a system it doesn't list. |
updates | A GitHub repository (owner/name). Installed copies update from its releases. Catalog apps leave it out. |
public | Folders the phone may load. Default ["ui", "assets"] plus the icons. |
required | true hides the Remove button (for an app that must always be there). |
The backend: index.cjs
function activate(ctx){
// ctx.dataDir: a folder for this app's files. ctx.electron, ctx.log, ctx.app.version too.
let count = 0;
return {
data(context){ return { count, items: [] }; }, // what the screen's bindings read
command(action, value){ // what the screen's actions send
if (action === 'add') count += value;
return count;
},
close(){} // stop timers, close windows
};
}
module.exports = { activate };
data()is called while your screen is open, every 1.5 s by default. The PC only sends it when it changed; for big data, include arevisionthat changes when the data does.command()may beasync. What it returns reaches the phone. Throw anErrorwith a readable message and the phone shows it.- Keep slow work in the background and report progress through
data()(aloadingflag), so commands answer quickly. - Optional:
island()puts your app on the PC island,settings.describe()gives the app its own category in the island's settings, andctx.island.present()briefly takes over the island.
Notifications
Tell the user something happened while your app isn't open:
ctx.notifications.post({ title: 'Build finished', body: 'main · 2 min' }); // returns its id
ctx.notifications.post({ id: 'inbox', title: '3 new messages' }); // the same id replaces it
ctx.notifications.clear('inbox'); // one of them; clear() with no id removes them all
ctx.notifications.count; // how many are unseen
Unseen notifications show as a dot on your app's icon on the phone and on the island, which lists them. Opening the app on the phone marks them seen. Onbun keeps the 20 newest per app. Only title is required; write it so it makes sense on its own.
The screen
ui/screen.json describes the phone screen from Kit components. Here's a complete one:
{
"type": "screen",
"title": "Hello",
"chrome": "fullscreen",
"accent": "teal",
"state": { "query": "" },
"bar": {
"leading": [{ "type": "label", "text": "$pc", "symbol": "desktopcomputer", "glass": true, "shape": "capsule", "padding": [0, 14], "height": 42 }],
"trailing": [{ "type": "button", "variant": "glass", "symbol": "plus", "action": { "command": "add", "value": 1 } }]
},
"body": {
"type": "scroll",
"children": [
{ "type": "each", "items": "$items", "as": "item",
"template": { "type": "row", "title": "$item.title", "subtitle": "{{item.created|age}}", "accessory": "chevron",
"action": { "cover": "detail", "params": "$item" } } }
]
},
"covers": { "detail": { "body": { "type": "text", "text": "$params.title", "style": "title2" } } }
}
- Design rules: Liquid Glass (
"glass": true,"variant": "glass") only for floating controls in the bar and over media. Content sits on flatcardandsectionsurfaces. "chrome": "fullscreen"gives your app the whole content area.height: "screen"with a snappingscrollmakes paging cards.sheetsandcoversare named extra pages, opened by actions.
Layout rules
These rules behave the same on iPhone and Android. Most layout bugs come from one of them.
The app background
The phone draws each app's background behind the whole app: under the floating bar, beside the rail, and to every edge of the screen. Set it on the screen, never inside body:
background: the screen color. Default black.backdrop: an image ("assets/backdrop.png", or a binding like"$artwork"for album art). The phone builds a soft gradient from its colors, darkened toward the bottom so controls stay readable, and cross-fades when it changes. Music, Agent Watch and Weather use it; Weather even generates its backdrop PNG on the PC to match the sky.surfaceandaccent: the color ofcardandsectionsurfaces, and of selected states.
A background drawn inside body (an image as the first child of layers, or a stack with a background or gradient) starts below the bar, so it leaves a band at the top and never reaches the edges.
The floating bar
bar floats over the content, with an optional accessory (like a search field) under it. Where the body goes depends on its top-level type:
- A body that is a
scrollor aliststarts below the bar and scrolls beneath it, so content slides under the glass. - Any other body (
stack,split,if,layers…) is placed below the bar.
So don't add top padding to clear the bar yourself, and keep the main scroll as the body instead of wrapping it in a stack.
Chrome and edges
"chrome": "standard"(the default) keeps Onbun's margins and its music volume bar."chrome": "fullscreen"gives the app the whole content area, down to the bottom edge. A scroll runs under the edge and keeps the margin as space after its last item. An element that fills the height with abackgroundorgradientextends it to the edge while its content stays inside the margin (Discord's call glow).
Full-screen pages
height: "screen" is the visible content height, including the strip behind the bar. Subtract the bar and your spacing with screenOffset: Reddit's paging cards use "height": "screen", "screenOffset": 106 with "snap": true on the scroll.
Landscape and portrait
The phone app is built for landscape (a rail on the left), but portrait works too, and each app has to look right in both. Read $env.landscape, $env.width and $env.height. A split sits side by side in landscape and stacks in portrait ("portrait": "stack", portraitHeight). Use fill, maxWidth and grid rather than fixed widths.
Glass and surfaces
The floating material ("glass": true or "variant": "glass") is Liquid Glass on iPhone and a tonal surface on Android. Use it only for floating controls: the bar's buttons, menus and pills, and controls over media. Content sits on flat card and section surfaces. Text reads best with style (largeTitle to caption2) and the semantic colors primary, secondary and tertiary.
Never by platform
$env.platform exists for analytics-style labels, not layout. An app never changes its layout per phone: if a component looks wrong on one phone, that phone's renderer gets fixed, so every app benefits.
Bindings and actions
Bindings read data
Any value can come from your backend's data(): "$items", "$item.title", or a template like "{{count}} hellos" with filters such as {{size|bytes}}. Logic uses operator objects: {"if": [{"gt": ["$count", 0]}, "Some", "None"]}.
Screen variables
What only the phone cares about (is search open, the text in a field) lives in state, read as $ui.name and changed with {"set": …} or {"toggle": …}.
Actions run on taps
command (to your backend), sheet, cover, dismiss, open, set, haptic… A list runs in order; a failing command stops it and runs its onError. Controls put their new value in $value.
Optimistic updates
[
{ "set": "pending", "value": true },
{ "command": "save", "onError": { "set": "pending", "value": null } },
{ "set": "pending", "value": null }
]
…and show {"coalesce": ["$ui.pending", "$saved"]}. Every action, operator and filter is in the reference.
Where names come from
A binding looks its name up in this order: names from each and let and the control's $value, then the roots ui (screen variables), params (what opened a sheet or cover), env (landscape, width, height, connected, error, platform) and data. Any other name reads your app's data. Start with $$ for a literal $.
Data and refresh
The phone asks the PC for your app's data while its screen is open. The PC calls data(context), compares the answer with the last one, and sends it only when it changed.
- Pace:
"refresh": {"interval": 1.5, "fast": 0.45, "fastWhen": "$loading"}(seconds) asks every 1.5 s, and faster while$loadingis true. Agent Watch uses{"interval": 2}; Weather{"interval": 5, "fast": 0.5, "fastWhen": "$loading"}. - Keep data() cheap: it runs every time the phone asks. Fetch from services in the background (a timer, with backoff when a service fails), keep the result, and return the cached copy. Return a
loadingflag while work runs, and anerrorstring the screen can show. - Large data: include a
revisionthat changes whenever the data does. When the phone already has that revision, only the revision travels. - Derived values: the screen's
letnames values worked out on the phone, like{"unread": {"count": "$messages"}}, read as$unread. Sheets and covers can have their own. - Moving media: playback positions keep moving between updates with
timelines:{"main": {"track": "$song.id", "position": "$song.current", "duration": "$song.duration", "playing": "$song.playing"}}.scrubberandlyricsfollow them, so data doesn't need to arrive every second. - Remembered choices:
"persist": ["theme"]keeps those screen variables on the phone between launches. - Lists: give items in
eacha stablekeyso animations and control state follow the right item. Ascrollcan run an action at itsend(load more) and on pull torefresh.
Commands
command(action, value) gets what an action sent. Return a result (the action can read it), or throw an Error with a sentence the user can act on. Keep it fast: start long work, return, and let data() report progress. Controls send their new value as value.
Web blocks
When the components aren't enough, drop in your own HTML: {"type": "web", "src": "ui/chart.html", "props": {"title": "Last hellos"}, "fill": "both"}. The page loads from your package and talks to the app:
onbun.onData(({ data, ui, props, params, env }) => render(data)); // now and on every change
const result = await onbun.command('add', 5); // like a command action
await onbun.set('query', 'cats'); // a screen variable
await onbun.run({ sheet: 'settings' }); // any Kit action
onbun.open('https://example.com'); // the phone's browser
Pages are transparent, so glass controls float over them. If the same web block shows up in several apps, it's a good candidate for a new native component.
Web blocks are a last resort: they don't get each phone's native look, controls or accessibility. Use them for a visual no component can draw, never for whole screens, lists, buttons or forms. The page inspector works on development builds: Safari → Develop → your iPhone, or chrome://inspect for Android.
Island and PC settings
On the island
Return island(context) from activate and your app can show on the PC's island. The island asks every app about once a second and draws the answers itself, so apps never ship island HTML.
island(){
return {
priority: 20, // higher wins the resting pill: e.g. 20 while playing, 10 when paused
compact: { title: 'Song', image: 'https://…/cover.jpg', accent: '#ff5a36', activity: true },
expanded: {
title: 'Song', subtitle: 'Artist', image: 'https://…/cover.jpg', placeholder: 'note',
buttons: [{ symbol: 'previous', action: 'previous', label: 'Previous' },
{ symbol: 'pause', action: 'toggle', label: 'Pause' }],
slider: { action: 'volume', value: 60, label: 'Volume', symbol: 'speaker', display: '60%' },
footer: { label: 'Open YouTube Music', action: 'setup', note: 'Runs on PC' }
}
};
}
Buttons, the slider (0 to 100) and the footer call your command(action, value), just as the phone does. Symbols: play, pause, next, previous, speaker, note, open, mic, mic-off, headphones, bell. Images must be https: or data:image/…. An app whose island() throws is simply left out. ctx.island.present() takes over the island briefly, like Clipboard does.
A settings page
Return settings: {describe(), action(action, values)} and the app gets its own page in the island's settings. describe() returns {status: {label, tone}, items: […]}, and the page refreshes while it's open.
| Item | Shows | action receives |
|---|---|---|
text · note | A paragraph, or a small grey note | |
steps | A numbered list (items) | |
link | An https: link (label, url) | |
input | A one-line field (id, label, placeholder, secret, value). Buttons send every field by its id. | |
toggle | A switch (label, detail, value, action) | {on} |
slider | A slider (min, max, step, display) | {value} when released |
editor | A row whose button opens a text editor (Save, or Ctrl+Enter) | {text} |
button | A wide button (primary, danger) | the fields |
Text values are cut to 2000 characters. Keep secrets like API keys here (an input with secret: true), on the PC, rather than on the phone.
Check and debug
- Live reload: apps in the apps folder reload within a second of any save, on the PC and on the phone.
- Screen errors: the PC checks every
screen.jsonagainst the component library before sending it. On a mistake the phone names the exact spot, likescreen: <text>.colour: … is not a text property. - From a terminal (in Onbun's source):
npm run check:apps -- path/to/your-appchecks the manifest and the screen. - In your editor: add
"$schema"pointing to screen.schema.json for autocomplete and inline errors. - Backend logs:
ctx.logwrites to Onbun's log on the PC. Errors thrown fromcommand()reach the phone as messages. - Try components first: the component explorer below renders any snippet, on an iPhone or Android frame, and shows what each tap would send.
Develop and publish
- Open your apps folder from the island: Settings → Apps → Open folder (
%APPDATA%\WinMore\apps) and drop your folder in. Onbun loads it right away and the phone adds it to its rail. - Edit any file. Onbun reloads the app within a second and the phone reloads its screen. No restart.
- If the screen has a mistake, the phone says exactly what to fix. Add
"$schema"pointing toscreen.schema.jsonfor autocomplete in your editor. - Share it as a folder or as a single
.wmappfile. Bumpversionon every release so installed copies update.
Make a .wmapp file
A .wmapp is your whole app folder in one file. Packing it checks it the way Onbun does before installing: the manifest, the entry module, file paths and sizes, and every Kit screen. Use whichever fits:
- From the island: Settings → Library → For developers → Export file…, then pick your app folder.
- From a terminal (Node 18 or later, nothing to install): download onbun-pack.cjs and run
node onbun-pack.cjs path/to/your-app. It writes<id>-<version>.wmappand explains any problem it finds. - Automatically on GitHub: save onbun-release.yml as
.github/workflows/onbun-release.ymlin your repository. Each time you push a version tag, it packs the app and attaches the file to a release:
git tag v1.2.0
git push origin v1.2.0
The tag has to match version in manifest.json. The workflow finds your app at the top of the repository or in its one subfolder that has a manifest.json.
Publish on GitHub
Attach the app's .wmapp to each GitHub release and tag it with the version (v1.2.0); the workflow above does both. Installed copies pick up the newest release like a catalog update. To be listed in the store, submit your app once it has a release; its store page gets an Add to Onbun button automatically.
Your own install button (optional)
To let people install from your own website or README too, link a button to one of these:
onbun://install?github=your-name/hello: the newest GitHub release (the app then updates from there)onbun://install?url=https://example.com/hello.wmapp: a package file over HTTPSonbun://install?app=hello: an app from the Onbun catalog
The island shows the app and where it comes from, and installs only after a yes.