For AI Agents
This page is written for coding agents rather than humans, so it is terse and imperative. Prefer
the patterns here verbatim, obey the invariants, and cross-check against the failure modes before you
finish. A machine-readable index of the whole site lives at /llms.txt.
Naming (disambiguate before writing)
- Day: the framework (proper noun; always capitalized in prose).
day: the CLI binary. You typeday build,day launch, etc. Always lowercase.day: the Rust crate.use day::prelude::*;brings in the whole API. Always lowercase.Day.toml: the project manifest. Piece: a UI node (SwiftUI View / Flutter Widget). Signal: a reactive state cell. target: an(OS, toolkit)pair, e.g.macos-appkit,ios-uikit.
What Day is (facts)
Day builds cross-platform desktop + mobile apps in Rust. You write one declarative UI as a tree of
Pieces; each Piece is realized by a native widget (NSTextField, UILabel, GtkEntry,
QSlider, XAML TextBox, android.widget.*) through a per-platform toolkit backend. Day owns layout,
reactivity, localization, accessibility policy, and scripting; the OS owns pixels, text input, scrolling,
and assistive tech. There is no virtual DOM and no diffing: the native tree is built once and Signals
bind straight to native attributes.
Invariants (MUST; violating these is a bug)
- One toolkit backend per binary. A binary compiles exactly one backend (selected by a Cargo
feature via the target). Never enable two. The build enforces this with a
compile_error!. - Views are built once, then kept live by bindings, never rebuilt. Never rebuild the view on state change. To make UI reactive, pass a
closure that reads a Signal (
label(move || …count.get()…)), or pass a Signal to a control (slider(volume)). Do not diff, re-run, or recreate Piece trees yourself. Signal<T>isCopy. Clone/move it into as many closures as you need; do not wrap it inRc.- Give every interactive/asserted Piece a stable
.id("…"). Tests, dayscript, and deep links address Pieces by id. No id ⇒ not scriptable. - Localize user-facing text with Fluent files. Scaffolded apps generate a typed function
per key (
res::str::my_key(), parameters become typed arguments), so a missing key or wrong arity is a compile error; prefer those over rawtr("key")(both exist). Don’t hard-code display strings in shipped apps (the showcase uses literals only for its own demo labels). Provider data (tickers, product names) stays verbatim. - Edit
Day.toml+ Rust; never hand-edit the generated Xcode/Gradle scaffolds.dayregenerates them. - Verify on a real target.
cargo builddoes not prove a target works. Useday launch -p <target>and, for assertions,day launch -p <target> --script <dayscript.yaml>.
Setup (canonical)
day new app my-app --toolkit macos-appkit,ios-uikit,android-mdc
cd my-app
day launch -p macos-appkit # build + run
day launch -p macos-appkit --script dayscript/walkthrough.yaml # build + run + assert
Day.toml (name/version come from Cargo.toml’s [package]):
schema = 1
[app]
id = "dev.example.my-app"
title = "My App"
targets = ["macos-appkit", "ios-uikit", "android-mdc"]
[window]
width = 480
height = 640
Core model (precise)
- A Piece is a value produced by a function call (
label(...),button(...),column((...))). Containers take a tuple of children. Builder methods (.padding,.spacing,.id,.font, …) return the Piece. End a heterogeneous Piece with.any()to getAnyPiece. Signal<T>:get()(tracked read),set(v),update(|v| …),with(|v| …)(borrow),get_untracked(). Reading a Signal inside a binding closure makes that binding re-run when the Signal changes; nothing else re-runs.- Reactivity rule: static content → pass a value; dynamic content → pass a closure.
label("Hi")is static;label(move || format!("{}", n.get()))is reactive. - A target is
(OS, toolkit):macos-appkit,macos-gtk,macos-qt,ios-uikit,android-mdc,harmony-arkui,linux-gtk,linux-qt,windows-xaml,windows-gtk,windows-qt,web-dom(wasm in a browser; no process environment, so pass runtime flags withday launch --env K=Vand read them withday::env("K"), which is portable to every target).
Canonical patterns (copy these)
App skeleton
use day::prelude::*;
fn main() {
day::launch(
WindowOptions {
title: "My App".into(),
size: Size::new(480.0, 640.0),
// WindowOptions grows fields over time — always spread the default.
..Default::default()
},
root,
);
}
fn root() -> AnyPiece {
let count = Signal::new(0i64);
column((
label(move || format!("{} clicks", count.get())).font(Font::Title).id("counter"),
row((
button("−").action(move || count.update(|c| *c -= 1)).id("dec"),
button("+").action(move || count.update(|c| *c += 1)).id("inc"),
))
.spacing(8.0),
))
.spacing(12.0)
.align(HAlign::Leading)
.padding(16.0)
.any()
}
Inputs (two-way; edits flow back into the Signal)
let name = Signal::new(String::new());
let volume = Signal::new(40.0);
let on = Signal::new(false);
column((
text_field(name).placeholder("Your name").id("name"),
slider(volume).range(0.0..=100.0).step(1.0).id("vol"),
toggle(on).id("on"),
progress(move || volume.get() / 100.0), // tracks the slider live
))
Conditionals + keyed collections
when(move || !name.with(|s| s.is_empty()),
move || label(move || format!("Hi, {}", name.get())))
// `each` builds one child per item and reconciles by key (each row keeps its own state).
// The row builder receives an ItemSlot (a Copy handle), NOT the item: read fields through it
// so recycled rows update when the backing item changes.
each(
move || items.get(),
|it| it.id.clone(),
|slot| label(move || slot.field(|it| it.title.clone())).id_keyed("row", slot.key()),
)
Navigation (a projection of an app-owned Signal; you own the state)
// one-of-N (Sidebar → split view; Tabs → native tabs):
let section = Signal::new(String::new());
selector(section)
.style(SelectorStyle::Sidebar)
.title("My App")
.item("home", "Home", home_page)
.item("settings", "Settings", settings_page)
.id("nav")
// push/pop stack bound to a path Signal:
let path = Signal::new(Vec::<String>::new());
stack(path, home_view).destination(|key| detail_view(key));
// push: path.update(|p| p.push("item-42".into())); pop is written back by the native back button.
navigate("settings"); nav_back(); current_route(); // string-route adapter (also deep links + dayscript)
stack(path, root).on_back(|req| if dirty.get() { BackResponse::Handled } else { BackResponse::Proceed }) // intercept back
Text, fonts, color, accessibility
label("Chapter").font(Font::Title).bold() // semantic style + weight
label("caption").font(Font::Footnote).italic()
label("18pt").font(Font::System(18.0)) // custom size, still accessibility-scaled
label(tr("greeting").arg("name", name)) // localized + interpolated Signal
progress(move || v.get() / 100.0).a11y(|a| a.role(Role::Meter).label("Volume"))
Semantic Font styles (largest→smallest): LargeTitle, Title, Title2, Title3, Headline, Subheadline, Body, Callout, Footnote, Caption, Caption2, plus System(pt). They map to the platform’s native text
styles and scale with the OS accessibility text size.
External Piece (native widget from a crate; no core edits)
use day_piece_combobox::combo_box;
let items = Signal::new(vec!["a".into(), "b".into()]);
let text = Signal::new(String::new());
combo_box(items, text).id("combo")
API quick reference
| Need | Use |
|---|---|
| static / reactive text | label("x") / `label(move |
| button | button("x").action(|| …) |
| text input | text_field(sig) · multiline: text_area(sig) |
| number input | slider(sig).range(a..=b) |
| boolean | toggle(sig) |
| choice | picker(opts, sig) · editable: external combo_box(opts, text_sig) |
| vertical / horizontal / z-stack | column((…)) / row((…)) / zstack((…)) |
| scroll · spacer · divider | scroll(child) · spacer() · divider() |
| conditional · list | when(cond, view) · each(items, key, row) |
| progress · busy | progress(frac) · spinner() |
| custom drawing | canvas(|d, size| …) (native 2D; Day never rasterizes) |
| nav (one-of-N / stack) | selector(sig) / stack(path, root) |
| localize | tr("key").arg("n", val) |
| accessibility | .a11y(|a| a.role(Role::…).label("…")) |
| identify for tests | .id("stable-id") / .id_keyed("row", key) |
CLI reference
day new app <name> --toolkit <t1,t2> # scaffold an app (bare `day new` = interactive)
day build -p <target> # compile
day launch -p <target> # build + run (streams stdout/stderr)
day launch -p <target> --script s.yaml # build + run + drive/assert
day pack -p <target> # installable artifact (.dmg / .ipa / .aab / .hap / flatpak / installer)
day lint # ids, Fluent coverage, project shape
day doctor # toolchains per target
day relaunch --all-running # stop + rebuild + relaunch — "apply my changes"
day stop --all # stop every recorded session
day drive -p <target> --steps-json … # drive a RUNNING app (see below)
day mcp-server # serve all of the above as MCP tools (stdio)
Verifying your work (dayscript)
Assert a running app with a cross-platform YAML script; Pieces are addressed by their .id, routes by
selector/stack keys.
name: check
flow:
- wait_for: { id: counter }
- tap: { id: inc }
- assert_text: { id: counter, text: "1 clicks" }
- navigate: { route: settings }
- assert_route: { route: settings }
- pause: { secs: 1.0 } # NOT `pause: 1s` — a mapping with float secs (or a bare int)
- screenshot: settings
For apps with network data, ship a deterministic mock behind a day launch --env MY_MOCK=1
flag (read it with day::env, which also works on web-dom) and write the walkthrough against
the mock’s exact values. Keep the mock’s arithmetic integer-derived. Transcendentals can
round differently across platforms and break cross-target assert_text on formatted numbers.
Driving a running app (day drive / MCP)
Every day launch embeds a loopback automation engine and records its coordinates in
build/day/sessions.json, so you can drive an app that is ALREADY running, without a script
file:
day drive -p macos-appkit --steps-json \
'[{"navigate":{"route":"settings"}},{"wait_idle":null},
{"tap":{"id":"save-button"}},{"assert_text":{"id":"status","text":"Saved"}},
{"screenshot":"after-save"}]'
Output is JSON (per-step ok/error, screenshot paths + base64). Step ops: navigate,
nav_back, tap, input, set_value, toggle, select, wait_for, wait_idle,
assert_visible, assert_text, assert_value, assert_route, assert_presented,
respond, a11y_audit, pause, screenshot.
If your host exposes MCP (VS Code agent mode does automatically in Day workspaces via the Day
extension), use the day_* tools instead: day_metadata, day_build, day_launch,
day_relaunch, day_stop, day_running, day_drive, day_screenshot, day_doctor,
day_lint. day_drive/day_screenshot return screenshots as images. Look at them to
verify UI changes on every target you touched. The canonical loop: edit → day_relaunch
(compile errors come back in the result) → day_drive (navigate + assert + screenshot).
Failure modes (do NOT do these)
- ❌ Enabling two toolkit features in one binary →
compile_error!. Enable exactly one via the target. - ❌ Rebuilding the view tree to reflect state. ✅ Bind a Signal (closure read or pass the Signal).
- ❌ Passing a
String/value where dynamic content is wanted. ✅ Passmove || …sig.get()…. - ❌ Wrapping
SignalinRc/Arc. ✅ It isCopy; move it directly. - ❌ Omitting
.id(...)on Pieces you need to test/script/deep-link. - ❌ Hand-editing generated
platform/ios/*.xcodeprojorplatform/android. ✅ EditDay.toml/Rust. - ❌ Concluding a target works from
cargo build. ✅day launch -p <target>(and--scriptto assert). - ❌ Declaring a UI change done without looking at it. ✅
day_drive/day drive→screenshot→ inspect. - ❌ Hard-coded pixel font sizes for shipping text. ✅ Semantic
Fontstyles (accessibility-scaled).
Deeper references
Human-oriented pages with the same facts in narrative form: Overview ·
Why Day · API tour · CLI & projects. Machine index:
/llms.txt.