Status: future · Priority: high · Depends on: Window API, Platforms (shipped)
Note: This is a future plan, not a commitment. The syntax and API shown here are proposals — they can be completely different when actually implemented.
Everything that makes an app feel like it belongs on the desktop instead of visiting from the web. Today these flows are reachable only by dropping into native.cpp — hand-rolled, untyped, and invisible to morph check. The plan: first-class JS APIs with the same registry safety as windows (every operation survives the user doing something unreasonable, like quitting mid-handshake).
Ctrl+Shift+Space that summons your app from any workspace is the difference between "installed" and "used daily"myapp://invoice/42 from an email should open invoice 42, not a second copy of your app staring blanklyWindow-scoped today (key events route to the focused node); global hotkeys fire even when the app is in the background:
import { Hotkey } from 'morph'
const summon = new Hotkey("ctrl+shift+space", () => {
mainWin.show()
mainWin.focus()
})
summon.register() // OS-level grab; throws a typed error on conflict
summon.unregister() // auto-cleaned when the owning window closesConflict with another app's hotkey is a result, not a crash — the API returns which registration won, because two apps fighting over Ctrl+Shift+Q is a tale as old as time.
// morph.config.json
{ "protocol": "myapp" } // registers myapp:// with the OS at install
// any route.mx — second launch arrives as props, not a second process
export default function InvoicePage(props: { invoiceId?: string }) { … }First launch opens normally; every later myapp://… invocation forwards its URL to the running instance, which routes it like a navigate. (Yes, this is just <a href> with extra steps — the OS is the link now.)
import { App } from 'morph'
App.requestSingleInstance((argv) => {
mainWin.show() // user double-clicked the icon: reveal, don't duplicate
mainWin.navigate(routeFromArgs(argv))
})Opt-in per app (a music player wants one instance; a text editor may genuinely want six windows). Without the lock, two processes share nothing — which is precisely the bug this prevents.
App.setLoginItem({ openAtLogin: true, args: ["--minimized"] })With --minimized support so tray-first apps don't flash a window on every reboot. Your users' autostart folders will thank you. (Their IT departments may not.)
<div onDropFiles={(files) => importFiles(files)}>
Drop invoices here
</div>files arrives as real paths (sandbox rules apply per platform), with hover-highlight state for free. The current alternative — an <input type="file"> — requires the user to find the file dialog, which is where imports go to die.
import { theme, setTheme } from './themeStore'
SystemTheme.onChange((mode) => setTheme(mode)) // 'light' | 'dark', follows the OSShips with a SystemTheme.current() for first paint, so the app never flashes the wrong theme on launch. (The flash of blinding white at midnight has ended more app trials than any bug.)
Taskbar.setBadge(3) // unread count on the dock/taskbar icon
Taskbar.setProgress(0.5) // download/install progress under the icon
Taskbar.clearBadge()Tiny API, huge "this app is alive" signal — the kind of thing users notice only when it's missing.
| Piece | State |
|---|---|
| Window-scoped key events (focused node) | ✅ Shipped |
Tray/hotkey/C++-driven flows via native.cpp |
✅ Shipped (untyped, manual) |
| Global hotkeys JS API | ❌ Not built |
| Protocol registration + URL forwarding | ❌ Not built |
| Single-instance lock | ❌ Not built |
| Login-item API | ❌ Not built |
onDropFiles + drop highlight |
❌ Not built |
SystemTheme listener |
❌ Not built |
| Badge / progress | ❌ Not built |
onSecondLaunch(url) handler per app?Hotkey register/unregister with conflict results (X11 → Win32 → Cocoa order, following Platforms)morph package + runtime URL handleronDropFiles event + drop-target highlight stateSystemTheme.current() / onChange