Crustini is a single-file-first creative coding language.
Its language direction is:
Processing immediacy with Rust-shaped structure when the sketch grows up.Artists write .flour source. Crustini parses it, checks it, lowers it into generated Rust, and runs it through a lightweight native preview host.
The active v0 language spec is new-spec.md.
This repository is in transition.
The product direction is the new .flour syntax in new-spec.md:
+++
crustini = "0.1"
name = "Sketch"
fps = 30
window = [640, 360]
+++
app! Main {
state {
x: number = 40;
}
fn draw() {
clear(Color::Black);
x += 2;
circle(x, 180, 24, Color::White);
}
}
The current compiler is still smaller than that target. It accepts .flour and still supports legacy implementation syntax in existing examples and fixtures. Treat that old surface as implementation compatibility, not the product direction.
Crustini v0 has one language with three teaching modes.
Sketch mode is the Processing-style entry point.
app! Main {
state {
x: number = 40;
}
fn draw() {
clear(Color::Black);
x += 1;
rect(x, 100, 24, 24, Color::White);
}
}
Rule:
If update() is omitted, draw() may mutate state.This keeps the first experience immediate, visual, and forgiving.
App mode is the recommended shape once behavior becomes more than a tiny sketch.
app! Main {
state {
x: number = 40;
speed: number = 120;
}
fn update() {
x += axis_x() * speed * dt();
}
fn draw() {
clear(Color::Black);
rect(x, 100, 24, 24, Color::White);
}
}
Rule:
If update() exists, mutation belongs in update().
draw() should be rendering-only except for short-lived local drawing calculations.Structured mode is for larger sketches, toys, and games.
Use struct, enum, match, and helper functions:
enum Mode {
Title,
Playing,
}
app! Main {
state {
mode: Mode = Mode::Title;
}
fn update() {
if pressed(Button::A) {
mode = Mode::Playing;
}
}
fn draw() {
clear(Color::Black);
match mode {
Mode::Title => {
text(40, 40, "PRESS A", Color::White);
}
Mode::Playing => {
text(40, 40, "PLAYING", Color::Green);
}
}
}
}
Keep app! Main { ... } as the visible compiler-level app form.
Normal runtime work should use normal function calls:
clear(Color::Black);
rect(x, y, 20, 20, Color::White);
pressed(Button::A);
dt();
Use :: for named constants, enum variants, and assets:
Color::Black
Button::A
Sprite::Player
Mode::Title
Use tiny beginner-facing primitive types:
number
text
boolDo not expose imports, modules, generics, traits, lifetimes, references, or ownership syntax in the v0 user surface.
A canonical .flour file has optional TOML front matter and Crustini source code:
+++
crustini = "0.1"
name = "Starter"
fps = 30
window = [640, 360]
+++
app! Main {
fn draw() {
clear(Color::Black);
text(24, 24, "hello crustini", Color::White);
}
}
Use plain +++ delimiters. Do not write +++ config or +++ sketch.
Starter files should include:
crustini = "0.1"
name = "Starter"
fps = 30
window = [640, 360]Project vocabulary is intentionally small.
| Product term | Meaning |
|---|---|
.flour |
Source extension for user-authored Crustini apps. |
rx |
Short command for running, proofing, baking, and emitting Crustini apps. |
main.flour |
Product-direction default file for a one-file sketch folder. |
recipe.flour |
Current compatibility project config for project-shaped apps. |
proof |
Check/validate source without writing a bakery or building. |
bake |
Build/compile action; build remains a compatibility alias. |
bakery |
Generated build workspace/cache; current directory is .bakery/. |
starter |
Project template. |
Use plain words for artifact, diagnostics, logs, generated Rust, and runtime.
The dense naming guide lives in docs/toolchain-vocabulary.md.
Product direction:
.flour source
-> Crustini compiler
-> generated Rust
-> lightweight native desktop appCurrent implementation shape:
rx app.flour
-> .bakery/
-> native preview window
rx bake app.flour
-> .bakery/
-> artifact onlyFor current project-shaped compatibility mode, recipe.flour plus src/main.flour can write generated Rust under .crustini/generated/.
The easy path opens a native preview window:
cargo run --bin rx -- examples/brick-breaker/app.flourAfter installing rx, that becomes:
rx examples/brick-breaker/app.flourInside a current project directory with recipe.flour, a bare rx opens that project:
rxUse proof to check source without writing a bakery:
rx proof examples/brick-breaker/app.flourUse bake when you only want the generated artifact:
rx bake examples/brick-breaker/app.flourEmit generated Rust to stdout:
rx emit examples/brick-breaker/app.flourFor headless checks and CI, set CRUSTINI_FRAME=1. That runs one frame through the preview host and writes a frame.ppm under the app bakery.
Build the Rust crates:
cargo buildThe root Bun workspace is the command layer for the whole repo:
bun run ci
bun run check
bun run build
bun run bake:example
bun run clean:dry
bun run clean
bun run port:clean 4321Command flow lives in scripts/*.ts; package.json is the command menu. The shell scripts in scripts/*.sh are compatibility wrappers.
bun install
bun run site:devThe Astro site lives in apps/site and imports the shared syntax package from packages/crustini-syntax.
Current starters still use the compatibility project shape:
cargo run --bin rx -- starter hello my-hello
cargo run --bin rx -- my-hello
cargo run --bin rx -- proof my-hello
cargo run --bin rx -- bake my-helloList starters with:
cargo run --bin rx -- startersThe current compatibility project layout is:
my-hello/
recipe.flour
src/
main.flour
assets/
.crustini/
generated/The product direction is simpler for beginners:
my-sketch/
main.flourDo not silently rename implementation paths until the CLI supports that shape.
apps/site Astro website and docs tools
crates/crustini rx CLI, project config, bakery orchestration
crates/crustini-lang .flour parser, app model, diagnostics, Rust emitter
crates/crustini-core no_std runtime crate used by generated apps
crates/crustini-host native std preview host for easy-mode rx run
crates/crustini-web-host parked browser preview host experiment
packages/crustini-syntax TypeScript syntax highlighting package
editors/vscode VS Code language extension
examples real runnable app examples
fixtures compiler and CLI source fixtures
scripts repo-level helper scriptsnew-spec.md: active v0 language direction.AGENTS.md: repository instructions for agents.docs/toolchain-vocabulary.md: naming and lifecycle notes.docs/basic-project.md: current simple project shape.docs/compatibility-parser.md: current compatibility parser layout.docs/target-spec/: early target-shape notes.docs/source-first-code-sharing.md: source-sharing principle.examples/compiling.md: current compile walkthrough.
- no full parser for the complete new v0 spec
- no LSP
- no tree-sitter
- no formatter
- no package manager
- no imports
- no modules
- no heap requirement
- no allocator requirement
The point of this repo is to prove this path:
simple Crustini source -> generated Rust compiles -> native preview runs