Documentation

Using Onbun. Building apps.

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 (arm64 for Apple silicon, x64 for 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

  1. Install the phone app: on Android, download Onbun-Android-<version>.apk from 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.
  2. Put the phone and the PC on the same trusted Wi-Fi or LAN, and open Onbun on the phone.
  3. 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.
  4. Connect to your PC then lists the PCs running Onbun nearby. Allow local network access when the phone asks, and choose yours.
  5. 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.
  6. 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+W or 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. Enter submits, Esc stops.
  • 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 .wmapp file, 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 .deb install 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 4317 and 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 an onbun:// 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"
}
FieldWhat it does
idA lowercase letter, then lowercase letters, digits or dashes (40 at most). Must equal the folder name.
versionSemver. Installed copies update when it goes up, so bump it on every release.
ui · screen"kit" and the path of the screen file.
symbolThe SF Symbol the phone shows until iconPNG arrives (Android draws the matching Material Symbol).
icon · iconPNGA black SVG glyph (Onbun shows it grey) and the white 120 px PNG made from it with npm run render:icons.
platformsAny of "win32", "darwin", "linux". Leave it out for all three. The app can't be installed on a system it doesn't list.
updatesA GitHub repository (owner/name). Installed copies update from its releases. Catalog apps leave it out.
publicFolders the phone may load. Default ["ui", "assets"] plus the icons.
requiredtrue 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 a revision that changes when the data does.
  • command() may be async. What it returns reaches the phone. Throw an Error with a readable message and the phone shows it.
  • Keep slow work in the background and report progress through data() (a loading flag), 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, and ctx.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 flat card and section surfaces.
  • "chrome": "fullscreen" gives your app the whole content area. height: "screen" with a snapping scroll makes paging cards.
  • sheets and covers are 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.
  • surface and accent: the color of card and section surfaces, 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 scroll or a list starts 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 a background or gradient extends 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 $loading is 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 loading flag while work runs, and an error string the screen can show.
  • Large data: include a revision that changes whenever the data does. When the phone already has that revision, only the revision travels.
  • Derived values: the screen's let names 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"}}. scrubber and lyrics follow 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 each a stable key so animations and control state follow the right item. A scroll can run an action at its end (load more) and on pull to refresh.

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.

ItemShowsaction receives
text · noteA paragraph, or a small grey note
stepsA numbered list (items)
linkAn https: link (label, url)
inputA one-line field (id, label, placeholder, secret, value). Buttons send every field by its id.
toggleA switch (label, detail, value, action){on}
sliderA slider (min, max, step, display){value} when released
editorA row whose button opens a text editor (Save, or Ctrl+Enter){text}
buttonA 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.json against the component library before sending it. On a mistake the phone names the exact spot, like screen: <text>.colour: … is not a text property.
  • From a terminal (in Onbun's source): npm run check:apps -- path/to/your-app checks the manifest and the screen.
  • In your editor: add "$schema" pointing to screen.schema.json for autocomplete and inline errors.
  • Backend logs: ctx.log writes to Onbun's log on the PC. Errors thrown from command() 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

  1. 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.
  2. Edit any file. Onbun reloads the app within a second and the phone reloads its screen. No restart.
  3. If the screen has a mistake, the phone says exactly what to fix. Add "$schema" pointing to screen.schema.json for autocomplete in your editor.
  4. Share it as a folder or as a single .wmapp file. Bump version on 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>.wmapp and explains any problem it finds.
  • Automatically on GitHub: save onbun-release.yml as .github/workflows/onbun-release.yml in 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 HTTPS
  • onbun://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.

Kit

Components

Every Kit component with an example you can edit. The preview is a rough browser version: switch between iPhone and Android to see each phone's look. On the phones they are drawn natively, with SwiftUI and Liquid Glass on iPhone and Compose and Material 3 Expressive on Android.

Reference

Generated from companion/kit.cjs, the same definitions the PC uses to check screens.