RatMinimal web framework

Native services

Native services are built-in globals whose backend touches the host: the filesystem, OS notifications, the share sheet, the system tray. Each one is written so the same call works on web, desktop, and mobile. It reaches a real device API where one exists and degrades to a web surface where it doesn't. They fall into two tiers.

The two tiers

Universal vs. native

Universal services (fs, notify, share, geolocate, camera) exist on every target with a graceful fallback, so they always work and need no declaration. Native services (tray) reach a capability only some targets can provide and have no fallback - an app that uses one must declare it in native_services, and declaring it on a target that can't deliver it aborts boot with a clear message instead of failing at the first call.

# settings.rat - opt into a native-tier service
native_services: ['tray']

fs - local files

Universal: disk on native, in-memory on web

fs reads and writes files through one contract: save / append / read / read_lines / exists / remove. On a desktop or mobile build it writes real files (rooted at a per-OS app-data dir, or a jailed fs_root); on the standard web build it falls back to a non-durable in-memory store, so the same source still runs as a website. For the web build, fs.export(filename?) downloads the whole store as one JSON bundle and fs.import() loads a bundle back - explicit, user-gesture persistence (and a backup/restore feature on durable builds).

> save_note[text]
    fs.append('notes.log', text + '\n')
    << fs.read('notes.log')

> backup[]
    fs.export('my-notes.json')

notify - OS notifications

Universal: native banner or in-page toast

notify.send(title, body) raises a real OS notification when the shell granted permission (desktop WebView2, mobile, and browsers that allow it), and otherwise shows a self-dismissing in-page toast. One call, a platform-appropriate surface everywhere.

> ping[]
    notify.send('Build done', 'All tests green.')

share - the share sheet

Universal: native sheet, Web Share, or clipboard

share.open(text, url) opens the native share sheet on mobile (and desktop where available); on the web it tries the Web Share API, then a clipboard copy, then a toast - so it always does something visible. The url argument is optional.

> share_link[]
    share.open('Check out Rat', 'https://rat-lang.com')

geolocate - device location

Universal: native GPS or browser geolocation

geolocate.get() starts a one-shot location request and returns immediately - the fix arrives asynchronously. On mobile/desktop with the native bridge it uses the device GPS; on the web it uses the browser's navigator.geolocation. Either way the result lands on the reserved __rat_geolocate channel as {lat, lng, accuracy} (or {error}), which your page subscribes to.

> locate[]
    geolocate.get()
    << 'requested'

camera - take a photo

Universal: native camera or getUserMedia

camera.capture() starts a capture and returns immediately; the photo arrives asynchronously. On mobile/desktop it opens the device camera through the native bridge; on the web it uses getUserMedia. Rather than push megabytes of image down the channel, the photo is saved into the app's fs store and only a lightweight {path: 'photos/<id>.jpg'} reference is broadcast on the reserved __rat_camera channel. Display it with an <img> pointed at the jailed /__rat__/fs/<path> route, which streams the bytes back from the store. Declare it with native_services: camera so the Android build requests the camera permission.

> snap[]
    camera.capture()
    << 'requested'

tray - the system tray

Native (desktop only): declare it

tray is the first native-tier service: a system-tray icon plus a menu, desktop-only. tray.menu([...]) sets the menu (each item is {id, label} or a bare string); clicking an item broadcasts {item: id} on the reserved __rat_tray channel - the same server-push model as notify - which your page subscribes to. tray.tooltip(text) sets the hover text. Because it's native-tier, an app that uses it must declare native_services: tray; that declaration aborts boot on web or mobile, where there is no tray.

# data/tray.rat
> service [tray_demo]
on_start[]
    tray.tooltip('My App')
    tray.menu([{id: 'open', label: 'Show window'}, {id: 'quit', label: 'Quit'}])

Declare what you use

Static detection

Forgetting to declare a native-tier service is easy to miss because it works on the target you're testing and silently no-ops elsewhere. At boot, Rat scans your source for native-tier usage: if you call tray.* without listing it in native_services, you get a warning naming the usage site and the fix, so a web or mobile build fails loudly via the declaration gate instead of quietly doing nothing.

[tier] native service "tray" is used (at service tray_demo.on_start)
       but not listed in native_services - add native_services: ['tray']

These services pair with the build targets in Desktop & mobile apps: write the call once, declare any native-tier capability, and the same app runs as a website, a desktop window, and a phone app.

See also Desktop & mobile apps · Channels · Services