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 type day 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)

  1. 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!.
  2. 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.
  3. Signal<T> is Copy. Clone/move it into as many closures as you need; do not wrap it in Rc.
  4. Give every interactive/asserted Piece a stable .id("…"). Tests, dayscript, and deep links address Pieces by id. No id ⇒ not scriptable.
  5. 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 raw tr("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.
  6. Edit Day.toml + Rust; never hand-edit the generated Xcode/Gradle scaffolds. day regenerates them.
  7. Verify on a real target. cargo build does not prove a target works. Use day 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 get AnyPiece.
  • 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 with day launch --env K=V and read them with day::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()),
)
// 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

NeedUse
static / reactive textlabel("x") / `label(move
buttonbutton("x").action(|| …)
text inputtext_field(sig) · multiline: text_area(sig)
number inputslider(sig).range(a..=b)
booleantoggle(sig)
choicepicker(opts, sig) · editable: external combo_box(opts, text_sig)
vertical / horizontal / z-stackcolumn((…)) / row((…)) / zstack((…))
scroll · spacer · dividerscroll(child) · spacer() · divider()
conditional · listwhen(cond, view) · each(items, key, row)
progress · busyprogress(frac) · spinner()
custom drawingcanvas(|d, size| …) (native 2D; Day never rasterizes)
nav (one-of-N / stack)selector(sig) / stack(path, root)
localizetr("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. ✅ Pass move || …sig.get()….
  • ❌ Wrapping Signal in Rc/Arc. ✅ It is Copy; move it directly.
  • ❌ Omitting .id(...) on Pieces you need to test/script/deep-link.
  • ❌ Hand-editing generated platform/ios/*.xcodeproj or platform/android. ✅ Edit Day.toml/Rust.
  • ❌ Concluding a target works from cargo build. ✅ day launch -p <target> (and --script to assert).
  • ❌ Declaring a UI change done without looking at it. ✅ day_drive/day drivescreenshot → inspect.
  • ❌ Hard-coded pixel font sizes for shipping text. ✅ Semantic Font styles (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.