App lifecycle, clipboard & openURL
The app's own lifecycle (foreground/background), the URL it was opened with (deep links), the
system clipboard, and opening external URLs. app and clipboard are plain global objects —
nothing to construct.
At a glance
app.addEventListener('pause', () => saveGame()) // user left — persist now
app.addEventListener('resume', () => refreshFeed())
if (app.launchUrl) openFromLink(app.launchUrl) // cold-start deep link
app.addEventListener('url', url => openFromLink(url)) // …and links arriving while running
clipboard.write('LECODES-42')
openURL('https://le.codes/docs')Lifecycle: app.state, pause / resume
app.state // "active" | "background" (read-only)
app.addEventListener('pause', () => { }) // app left the foreground
app.addEventListener('resume', () => { }) // app came back
app.removeEventListener('pause', callback) // same function reference"pause" fires when the app stops being the foreground app — home button, another app on top,
the browser tab hidden. It is delivered before the host halts the frame loop, so it's the
last reliable moment to persist state, pause music, or stop work that shouldn't run blind
(a Geolocation watch, polling timers). "resume" fires when frames are running again.
The events are semantic and identical on every platform — you never see Activity/Scene
vocabulary. app.state is the same information as a pull, readable at any time.
don't count on code running while backgrounded — on mobile the frame loop stops
and timers freeze until resume. pause is for saving, not for scheduling background work.
Deep links: app.launchUrl + the url event
app.launchUrl // string | null (read-only)
app.addEventListener('url', (url: string) => { })launchUrl is the URL the app was (most recently) opened with — null for a plain launch.
When a link arrives while the app is already running (a warm link), launchUrl updates to
the new value and the "url" event fires with it. Handle both, in that order:
const openFromLink = (url: string) => {
const screen = new URL(url).searchParams.get('screen')
if (screen === 'stats') router.push(StatsScreen())
}
if (app.launchUrl) openFromLink(app.launchUrl)
app.addEventListener('url', openFromLink)host-gated — launchUrl reads null and "url" never fires on hosts without a
link concept (headless).
World control: app.restart / app.quit
app.restart(url?) // re-run this app's bundle in a fresh world — location.reload()
app.quit() // leave to the LeCodes launcher (home) — no-op elsewhererestart() reboots the app from scratch: fresh world, same code. Pass a url to reboot with
different launch arguments (the new run reads them via app.launchUrl), pass null to reboot
with a cleared one, or omit it to replay the current one — the argument mirrors launchUrl's
role as the app's "command line". Unsaved state is gone, exactly like a page reload.
quit() returns the user to the LeCodes launcher. In a standalone-built app there is no
launcher, so it does nothing (platforms forbid programmatic exit) — safe to wire to a
"back to LeCodes" button unconditionally.
UIButton(UIText('Start over')).onClick(() => app.restart(null))
UIButton(UIText('Exit')).onClick(() => app.quit())Keyboard: app.keyboardHeight + the keyboard event {#keyboard}
app.keyboardHeight // number (read-only, logical px; 0 when hidden)
app.addEventListener('keyboard', (height: number, duration: number) => { })keyboardHeight is the on-screen keyboard's raw overlap with the app viewport — independent
of the focused input's keyboardShrink
policy. With the default shrink policy the layout already avoids the keyboard, so you rarely need
it; its main consumer is overlay mode (keyboardShrink: false), where the app manages its own
inset. The event fires on every keyboard frame change; duration is the platform's keyboard
animation duration in ms (0 where none) — pass it to animateTo to move UI in sync:
// chat composer: keyboard overlays the UI, the composer slides itself up
input.style({ keyboardShrink: false })
app.addEventListener('keyboard', (height, duration) => {
composer.animateTo({ transform: `translateY(${-height}px)`, duration })
})host-gated — reads 0 and never fires where no on-screen keyboard exists (macOS,
desktop, hardware keyboards).
clipboard
clipboard.write(text: string): void // fire-and-forget
clipboard.read(): Promise<string> // rejects: no access / denied / nothing readablewrite puts text on the system clipboard (a silent no-op where there's no clipboard). read
is async because the web host gates it behind a permission prompt; expect and handle rejection:
clipboard.write(inviteCode)
toast('Copied!')
try { input.value = await clipboard.read() }
catch { toast('Nothing to paste') }openURL
openURL(url: string): voidOpen url in the system browser / external handler — fire-and-forget, like share.
Use it for "Terms of service", "Rate us", mailto: links. A silent no-op on hosts without one
(headless).
UIButton('Privacy policy').onClick(() => openURL('https://le.codes/privacy'))See also
- Device —
device.online+online/offlineevents (connectivity) - Geolocation — stop watches in
pause - Storage — what to persist when
pausefires