What a script app's main.splash can use when it runs in a policed isolate
(App Hub card-host and the shells' Card runner). Everything here was read in
the runtime source (OctoSense-org/makepad sandbox/contained-tier-gates at
d94e5e6, merged to main as #30, cd812acd; the file:line references
were checked at cd812acd, the pinned runtime is now c155f61d) and, where
marked ✓ run, executed in
card-host (App Hub 79a2c4f) on macOS. Idioms are taken from the System
Apps (OctoSense apps/<name>/bundle/main.splash), which are
working code.
If something is not on this page, check the runtime source before using it. If it is not there either, it does not exist for apps.
Paths below: MP = the makepad checkout, HUB = OctoSense-App-Hub crates/.
let notes = [] // state: top-level let, persists for the session
fn load(){ // functions may use ui.* (they run after the body)
if fs.exists("notes.json") {
let v = fs.read("notes.json").parse_json()
if v != nil { notes = v }
}
ui.list.render()
}
start_timeout(0.05, || load()) // ui is injected AFTER the body evaluates
SolidView{width: Fill height: Fill flow: Down padding: 16 draw_bg.color: #xffffff
list := ScrollYView{width: Fill height: Fill flow: Down on_render: || {
if notes.len() == 0 { Label{text: "No notes yet." draw_text.color: #x8e8e93} }
for i in notes.len() { Label{text: notes[i] draw_text.color: #x1c1c1e} }
}}
}
- Declarations (
let,fn) first, then one root widget. The host prependsuse mod.prelude.widgets.*,let fs = mod.fs,let host = mod.host(anduse mod.netwhen the app may use the network), then continues an openView{height: Fit, …}with your body (MP/widgets/src/splash.rs:149-152). uiis injected after the body is evaluated (splash.rs:367-371). It works inside everyfnand handler, but not in top-level statements during the first evaluation. Start work withstart_timeout(0.05, || boot()), as every System App does. ✓ runname := Widget{…}makes a widget addressable asui.name. A:=id is a field of its direct parent only; name every wrapper you need to reach through.{{assets}}in the source is replaced by the loopback origin serving the bundle (HUB/app-policy/src/entry.rs), and that origin is on the app's host list:Image{src: http_resource("{{assets}}/thumbs/a.jpg")}, orlet assets = "{{assets}}"thenhttp_resource(assets + "/" + name)(Photos).
Lookup searches the handler's subtree, then the whole app; ui.root is the
root. A miss is an error widget '<id>' not found in tree; an unknown method
is widget method <m> not found for uid <uid> (MP/widgets/src/widget_async.rs:1248-1331, 1455-1460).
| Call | On | Notes |
|---|---|---|
ui.x.text() / ui.x.set_text(s) |
any widget (widget.rs:792-808); Label, LinkLabel, TextInput, buttons |
set_text wants a string: set_text("" + n). ✓ run |
ui.x.set_visible(b) / ui.x.visible() |
any widget (widget.rs:765-787) |
Views take a bool only (view.rs:877-892). ✓ run |
ui.x.render() |
View, SolidView, RoundedView, ScrollYView, ScrollXView | re-runs the view's on_render. ✓ run |
ui.x.set_src(http_resource(url)) / set_src(nil) |
Image (image.rs:295) |
the url must pass the media rule (see Network) |
ui.x.load_image_from_data_async(bytes) |
Image (image.rs:324) |
e.g. fs.read_bytes("DCIM/IMG_1.jpg") (Camera) |
ui.x.value() / set_value(n) |
Slider (slider.rs:1620) |
no script callback |
ui.x.checked() |
CheckBox (check_box.rs:519) |
no setter |
ui.x.on_click() |
Button family (button.rs:548-583) |
fires the handler |
open() close() toggle() is_open() |
SlidePanel (slide_panel.rs:81-97) |
| Handler | Widget | Arguments | Example |
|---|---|---|---|
on_click: || … |
Button, ButtonFlat, ButtonFlatter | none | ButtonFlat{text: "Add" on_click: || add()} ✓ run |
on_click: |active| … |
CheckBox | active |
|
on_change: |text| … |
TextInput | current text | News search. text is the new text; ui.<its id>.text() returns the same ✓ run |
on_return: |text| … |
TextInput | current text | Maps search |
on_tap: |x, y| … |
GestureView | point in the view | GestureView{on_tap: |x, y| remove(i) …} ✓ run |
on_double_tap: |x, y|, on_long_press: |x, y| |
GestureView | News long-press | |
on_swipe: |dx, dy| |
GestureView | one axis is 0 | Photos |
on_pan: |dx, dy, phase| |
GestureView | phase 0 begin, 1 move, 2 end | |
on_pinch: |scale, x, y, phase| |
GestureView | Photos | |
on_detent: |d| |
SheetView | 0 peek, 1 half, 2 full | |
on_render: || { children } |
View family | none; the closure's widgets become the children | the template's list ✓ run |
on_capture, on_error, on_zoom: |z| |
CameraPreview | Camera | |
on_error |
WebReader | none | News |
GestureView thresholds: tap slop 10 points, swipe at least 60 points within
0.45 s, double tap within 0.3 s (gesture_view.rs:57-66).
Bare names from std (MP/platform/src/script/timer.rs):
| Call | Returns |
|---|---|
start_timeout(secs, fn) |
timer id. ✓ run |
start_interval(secs, fn) |
timer id |
stop_timer(id) |
– |
time_now() |
Unix seconds (float). ✓ run |
local_time(epoch?) |
{year month day hour minute second weekday} (weekday 0 = Sunday). card-host sets no UTC offset, so it is UTC there. ✓ run |
random() / random_u32() / random_seed() |
0..1 / u32 / reseed. ✓ run |
let rec_timer = nil
fn start_rec(){ rec_timer = start_interval(0.5, || tick_rec()) }
fn stop_rec(){ stop_timer(rec_timer) }
A body containing fn tick( gets an implicit 1 Hz interval calling tick
(splash.rs:381-397). Name your function something else unless you want that.
Math is bare too: floor ceil round abs min max clamp pow sqrt sin cos …
(floor(2.7) = 2 ✓ run).
The app's jail (<app-data>/<id>/; in card-host <app>/.local-state/<id>/).
Paths are relative to it; a leading / means the jail root; .. above the
root is an error; symlinks are refused (MP/widgets/src/splash_storage.rs).
Only an app granted storage has a jail. Without it every call below errors
with storage not available in this context (see
CAPABILITIES).
| Call | Returns | |
|---|---|---|
fs.read(path) |
string | ✓ run |
fs.read_bytes(path) |
u8 array | |
fs.write(path, text) |
nil | ✓ run |
fs.append(path, text) |
nil | ✓ run |
fs.exists(path) |
bool | ✓ run |
fs.list(dir) |
sorted names, directories end in / (["b/"]) |
✓ run |
fs.mkdir(path) |
nil, recursive | ✓ run |
fs.remove(path) |
nil; a file or an empty directory | ✓ run |
No rename, stat or binary write. Limits: 1 MiB per file, the manifest's
storage.max_bytes for the whole jail (ceiling 16 MiB installed, 64 MiB
system), 256 entries, depth 16, 128-character names. A failure returns an
error value (not nil) and is logged; the messages include file not found,
path escapes the app's storage, file too large, app storage is full,
too many files, data must be a string, not a directory.
fs exists whenever the host gives the isolate a jail (card-host always
does), independent of the storage capability; request storage anyway when
you store anything, because the store's privacy summary is derived from it.
Available only when the app has net and at least one host in
network.hosts; otherwise net is not defined at all
(variable net not found in scope … ✓ run).
fn fetch(url){
let p = promise()
net.http_request(net.HttpRequest{url: url method: net.HttpMethod.GET headers: {"User-Agent": "MyApp/1.0"}}) do net.HttpEvents{
on_response: |res| p.resolve(res)
on_error: |_err| p.resolve(nil)
}
p
}
fn refresh(){
let res = fetch("https://api.example.com/today").await()
if res == nil || res.status_code >= 400 { ui.status.set_text("Offline") return }
let data = res.body.to_string().parse_json()
…
}
- Request:
net.HttpRequest{url method headers body …}; methodsnet.HttpMethod.GET HEAD POST PUT DELETE PATCH …(MP/platform/network/src/types.rs). - Events:
net.HttpEvents{on_response on_error on_stream on_complete};res.status_code,res.headers,res.body(bytes;.to_string());err.message. Default response cap 16 MiB (MP/platform/script/std/src/net.rs). promise(),p.resolve(v),p.await()(News).- A url outside the rules is refused without calling
on_error: the call returns the errorthis app may not reach <url>. Image{src: http_resource(url)}loads a picture; refused:this app may not load <url>and the log lineScript resource refused by the host's allowlist: <url>.
The rules (MP/widgets/src/splash_policy.rs): requests reach exactly the hosts
in network.hosts (lowercase, exact, no wildcard). Pictures (http_resource)
may also come from any public https:// host when the app has images; web
pages (WebReader) when it has web. Private, internal and non-https addresses
are refused (only https:// URLs are allowed, host not permitted (private/internal): <h>).
The gate additionally refuses a bundle whose source names an undeclared https
host (PUBLISHING §2).
net.web_socket is held to the host list like net.http_request
(this app may not reach <url>). net.socket_stream and net.http_server
are refused in a store app, whatever it was granted (this app may not open a raw socket, this app may not open a listening server; makepad#30, net.rs
at the runtime this repository pins).
host.request("mail.list", {account: account.id folder: folder.id offset: 0 limit: 100}, fn(r){
if r.is_ok { messages = r.data.messages ui.inbox.render() }
else { ui.status.set_text(r.error) }
})
host.request(service, args, fn(r))returns a request id.r.is_ok(bool;okis a keyword),r.data(parsed JSON or nil),r.error(string or nil) (MP/widgets/src/splash_host.rs:246-318).- Service
a.bneeds capabilitya(or exactlya.b). Without it the callback runs immediately withis_okfalse andr.error=this app was not granted "mail", which "mail.accounts" needs; the log showssplash host: refused "mail.accounts": this app was not granted "mail", which "mail.accounts" needs. ✓ run - Every request answers once (App Hub#38, in the OctoSense shells since
OctoSense#243; not run for this page). A call that waits longer than
its service allows (60 s unless the service asks for more; the clock stops
while the service's sheet is up) answers
the host service timed out; a 33rd waiting call from one app answerstoo many host requests are waiting; try again when some have answered; from a home-screen tile or an assistant's tool call, a service that needs its sheet refuses with a sentence such asthis surface cannot raise a prompt; open the app to continue(in the shells since OctoSense#204, merged 2026-10-01, for glance tiles and agents' tool calls). The limits: App Hub PUBLISHING § Host services and sheets. host.capabilities()→ the granted list (["storage"]✓ run);host.has("net")→ bool (✓ run). There is nohost.prompt.- Services, sheets and the Mail methods: HOST-SERVICES.
| Available | Notes | |
|---|---|---|
| JSON | s.parse_json(), v.to_json() |
parse_json never raises. Invalid JSON does not reliably give nil ("{nope".parse_json() == nil was false ✓ run): check the fields you need. to_json stops at depth 24. |
| Feeds | s.parse_feed(style) |
→ [{title link source published summary image}] or nil (mod_feed.rs); News |
| Strings | len trim split(p) search(p) replace(p, r) strip_prefix strip_suffix url_encode url_decode to_f64 match_str match_all |
to_f64 gives NaN (not nil) for "", non-numbers and untrimmed text (" 12.5 "); NaN compares false with everything, so guard with if v >= 0 (✓ run). search → index or -1. "a-b-c".replace("-", "+") = a+b-c: first occurrence only ✓ run; replace all with regex("-", "g"). |
| Regex | regex(pattern, flags) with .test(s), .exec(s) |
case-insensitive contains: regex("word", "i").test(s) (News) |
| Missing | to_lower to_upper contains starts_with ends_with |
use search(p) == 0, search(p) >= 0, or a regex |
| Arrays | push pop clear len remove(i) retain(fn) |
no sort map filter join insert index_of slice |
| Objects | o.k, o[k], delete, extend, o += {k: v} |
|
| Numbers → text | "" + n |
|
| Loops | for i in n (0..n-1), for v in array, for i v in array, for k v in object, a..b ranges, loop {}, while |
there is no range(). ✓ run (for i in 3 sums to 3; for k v in {a:1 b:2} gives ab) |
Every name in the widgets prelude resolves in the isolate
(MP/widgets/src/lib.rs:518-523). None is gated to exist; gates apply when a
widget acts.
| Widget | Use | Gate |
|---|---|---|
View, SolidView, RoundedView |
containers; SolidView/RoundedView for a filled background |
– |
ScrollYView, ScrollXView, ScrollXYView |
scrolling containers, often with on_render |
– |
Label, LinkLabel |
text (default text color is white: set draw_text.color) |
– |
TextInput |
text entry; give it a numeric height |
password / one-time-code fields refused (see Gotchas) |
Button, ButtonFlat, ButtonFlatter |
buttons with text: and on_click |
– |
Image |
src: http_resource(url), fit: ImageFit.CropToFill |
media rule (see Network) |
Html, Markdown |
rich text | – |
GestureView, SheetView |
taps, swipes, pinches; bottom sheet with detents | – |
Hr, Vr, Icon, LoadingSpinner, Slider, CheckBox, Toggle, RadioButton, DropDown |
controls | – |
HostedView |
full: / tile: faces for the home screen (News, Photos) |
– |
WebReader |
open(url)→bool close() is_open() error() url() |
url on the host list, or any public https page with web; refused: refused <url>: not on this app's host list, and no \web` grant` |
CameraPreview |
start() stop() switch() capture() record_start() record_stop() set_zoom(n) set_flash(s) focus(x,y) last() error() …; captures land in the jail at DCIM/ |
camera; refused: this app was not granted the camera. microphone adds sound, library copies to the photo library |
MapView |
map with set_nav_polyline etc. (Maps) |
tile hosts must be declared; follow camera needs location |
glass.* |
Liquid-glass kit (glass.Card, glass.GlassButton, …) |
– |
Registration lines are in the source (view_ui.rs, label.rs, button.rs,
image.rs, gesture_view.rs, web_reader.rs, camera_preview.rs,
map/view.rs, glass_panel.rs, …). MapView needs the host built with the
maps feature (card-host is).
sys.* (weather, stock, geocode, route, gps, …) is registered by card-host
(register_agent_module) and may be absent in other hosts; every fetch it
makes is held to network.hosts, sys.gps reads "no fix" without
location, and profile-backed helpers (link, prefs, watchlists) return
empty because no app can hold profile. Prefer net.http_request to hosts
you declare.
| Limit | Value | Message |
|---|---|---|
| One handler, timer, callback or the body | 200 000 instructions and 64 ms | script instruction limit exceeded (✓ run: an endless loop in a timer; the app kept running) / script time budget exceeded |
| Cumulative instructions per session | manifest compute.instruction_budget, ceiling 20 000 000 (system 4 000 000 000); charged for the body, host callbacks and tick |
splash: <label> is stopped: its instruction budget is spent; afterwards requests, network and media are refused |
| Heap | compute.memory_bytes, ceiling 64 MiB (system 128 MiB) |
script heap allocation limit exceeded while <op>: requested <n> bytes, <m> remaining |
| Storage | see Storage | app storage is full, file too large, too many files |
| HTTP response | 16 MiB |
Sources: MP/widgets/src/widget_async.rs:15, 456-465, MP/platform/script/src/vm.rs:895-1049,
MP/widgets/src/splash_policy.rs:238-252, HUB/app-policy/src/policy.rs:33-62.
- Comments depend on the language: native Splash uses
//and/* … */.#starts color tokenization; it is not a prose-comment prefix. Copying.card-style#comments into a Splash function can interrupt initialization after some fields have already been filled. See the archived tokenizer/VM diagnostic. - Container boundaries:
View{width: Fill height: Fit}is already closed. Indented widgets on following lines do not become its children. Keep the container open through its intended children, and check for an extra closing brace left after a layout edit. Open success alone does not prove a complete root or startup callback; follow the validation loop. - Hex colors: write
#xbefore any hex color with anenext to a digit (#x1e1e2e,#x2ecc71);#xis always safe. Otherwise the tokenizer reads an exponent. - No
range():for i in n. on_renderempty state:if list.len() == 0 { Empty } for i in list.len() { Row }. Withif … {…} else for …the empty branch drew nothing and stale rows stayed on screen (✓ run with the template; fixed there).- Backgrounds:
View{show_bg: true draw_bg.color: …}did not draw its background incard-host, whether always visible or shown later withset_visible(true)(✓ run);SolidView{draw_bg.color: …}did. UseSolidVieworRoundedViewfor filled panels. - Default button look: a
ButtonFlatwith onlytext:draws white text on a light outline, invisible on a white app (✓ run). Style it as the template does (draw_bg +: {color: … color_hover: … color_down: …}anddraw_text +: {color: … color_hover: … color_down: …}). To reuse a style, bind it once and instantiate it:let Chip = ButtonFlat{height: 40 draw_bg +: {…} draw_text +: {…}}, thenChip{text: "10%" on_click: || set_tip(10)}(✓ run). - A
TextInputinside its ownon_change: reading the input itself (ui.<its id>.text()) there works since makepadd0a9def5and returns the edited text (✓ run). Before it, this failed withwidget has no uidand thennot found in tree(OctoScript-Makepad#44); if you see those errors, update the runtime (tools/setup-native.py --update). Passing the handler'stextargument on (on_change: |text| set_query(text), as News and Photos do) remains the simplest form. ButtonFlatholds no children: aLabelinside aButtonFlatis not drawn and the button's own text disappeared (✓ run). Usetext:; for a rich tappable row useGestureView{on_tap: |x, y| …}around the content.- Password fields are refused in a policed isolate:
is_password: trueorcontent_type: TextInputContentType.Password | NewPassword | OneTimeCoderenders an inert field readingApps can't ask for passwords(✓ run), with no log line, and the gate refuses the bundle. - Missing fields and names are errors, not nil:
property missing_field not found in prototype chain. Did you mean: …andvariable no_such_thing not found in scope. Did you mean: …(✓ run). Guard optional fields with checks on data you control. - Shadowing builtins: a top-level
let/fnthat reuses a prelude name (tick,floor, a widget name) opens a child scope rather than replacing it, andfn tick(starts an implicit timer. Pick distinct names. - Reserved words cannot be variable names:
me scope self nil true false ok let var mut fn if elif else for in while loop match return break continue and or is do try use. local_time()is UTC in card-host.- Text is white by default. Set
draw_text.coloron light backgrounds. TextInputneeds a numeric height; aFillheight inside aFitparent collapses to zero.
card-host prints to stdout (tools/octo run --detach writes
<app>/.local-state/card-host.log).
- Runtime errors in handlers, timers and callbacks:
[E] splash:<n>:<line>:<col> - <message> (<runtime file>:<line>).<line>is themain.splashline plus 3 (plus 4 whennetis granted), because of the prelude. Verified: an error onmain.splashline 2 printed as[E] splash:35135094784:5:26 - property missing_field not found …. - A body that fails to evaluate logs
splash: splash:<n>:<line0>:<col0>: <message>with 0-based positions (subtract 2, or 3 withnet). Non-fatal errors during a successful body evaluation are not printed. - Other lines worth grepping:
card-host: … admitted,card-host: refused:,[SPLASH] eval:,on_render closure failed; discarding its output:,splash host: refused,splash host callback error:,contained a mini-app isolate panic.