diff --git a/README.md b/README.md
index 377f9db..15161c2 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,24 @@
# awan ☁️
-[](https://github.com/codewithwan/awan/actions/workflows/ci.yml)
-[](#license)
-[](https://codewithwan.github.io/awan/)
+[](https://codewithwan.github.io/awan/)
+[](https://github.com/codewithwan/awan/actions/workflows/ci.yml)
+[](https://crates.io/crates/awan-cli)
+[](#license)
-> A tiny living character for your terminal — and a **personality layer** any
-> CLI can embed: `wait`, `ask`, `react`.
+A tiny pixel character that walks your GitHub contribution year.
-
+
+He reads your real numbers, brags when the month went well, and goes to sleep
+when it didn't. A workflow in **your** repo redraws him nightly — nothing to
+host, no account, no token.
+
-He strolls, bonks into crates, dozes off mid-sit, chases butterflies, freezes
-at falling gems, fetches his little oven to bake (and devour) a cake, dances
-to a silent beat, juggles a ball until it bonks him dizzy, builds a rocket,
-and watches it explode — then shakes off the soot and strolls on. On the very
-first run, he hatches out of an egg. 🥚
-
-## Quick start
+## Put him on your profile
+
+Three files in the repo named after you. No secrets to set up: the token GitHub
+Actions already gives you reads everything he needs.
+
+
+
+
+
+
+
+ A real profile, not a mock-up: the streak, the readout and the contribution
+ year are all live numbers a workflow fetched. It's a still preview, so it
+ isn't refreshed — yours would be, nightly.
+
+
+
+**[Build it in the browser →](https://codewithwan.github.io/awan/)** Arrange the
+beats, watch it play, download the folder. The preview runs the real engine
+compiled to wasm, so the frames you see are the frames CI draws — and you can
+draw your own character while you're there.
+
+Nothing is stored and there's no server behind it. Your config lives in your
+repo and the workflow runs there, which is also why this page can't break your
+banner.
+
+Prefer files? Copy the ready-made setup and edit one:
+
+```sh
+cp -r profile/sample/. my-profile/ # awan.json + a GitHub Action + a profile README
+cargo run -p awan-profile -- whoami --config my-profile/awan.json
+```
+
+Full walkthrough and the `awan.json` format: **[`profile/`](profile)**. Built as
+a separate, opt-in crate, so the core `awan` stays untouched.
+
+## He also lives in your terminal
+
+The banner is a side effect: awan is a character engine first, and the same
+character runs in a terminal, reacts to your shell, and can be embedded by any
+CLI that can spawn a process.
```sh
npx @codewithwan/awan demo # try it, no install (needs Node)
@@ -125,50 +158,6 @@ Runnable, self-contained examples for each language are in
[**`usage/`**](usage) — `cd usage/node && npm install && npm start`, and so on.
From your code it's just `awan.react("task.done")`; you never spawn anything.
-## Profile GIF
-
-Turn awan into a **seam-free looping banner for your GitHub profile** — he walks
-in and tells your story (builds a rocket, bakes, warms up by a campfire, prints
-your numbers, walks his own contribution year, sings karaoke, kicks a ball,
-naps), then loops. It's all driven by one editable `awan.json`, scene order
-included.
-
-
-
-
-
-
-
- A real profile, not a mock-up: the streak, the readout and the contribution
- year are all live numbers a workflow fetched. It's a still preview, so it
- isn't refreshed — yours would be, nightly.
-
-
-
-### Build one without writing any JSON
-
-**[codewithwan.github.io/awan](https://codewithwan.github.io/awan/)** — arrange
-the beats, watch it play, take the three files. The preview is the engine itself
-compiled to wasm, so the frames you're looking at are the frames CI will draw.
-
-Nothing is stored and there's no server behind it: your config lives in your
-repo, and the workflow runs there. That's also why the page can't break your
-banner.
-
-
-
-Prefer files? Copy the ready-made setup and edit one:
-
-```sh
-cp -r profile/sample/. my-profile/ # awan.json + a GitHub Action + a profile README
-cargo run -p awan-profile -- whoami --config my-profile/awan.json
-```
-
-Full walkthrough and the `awan.json` format: **[`profile/`](profile)**. Built as
-a separate, opt-in crate, so the core `awan` stays untouched.
-
## Characters
Characters are plain TOML — pixel rows plus a palette, **zero Rust**:
diff --git a/web/crate/src/lib.rs b/web/crate/src/lib.rs
index 9733096..aa37125 100644
--- a/web/crate/src/lib.rs
+++ b/web/crate/src/lib.rs
@@ -84,9 +84,23 @@ impl Preview {
}
}
+/// Why a spec won't load, in the engine's own words — or `None` if it will.
+///
+/// The editor used to re-implement these rules in JavaScript, which meant two
+/// sources of truth and one of them wrong: a character with no eyes fell back
+/// to the built-in buddy *silently*, so you drew a cat and watched a cloud walk
+/// past. The rules live in one place. Ask them.
+#[wasm_bindgen]
+pub fn check_spec(toml: &str) -> Option {
+ match awan_core::spec::parse(toml) {
+ Err(e) => Some(e.to_string()),
+ Ok(spec) => Character::from_spec(&spec).err().map(|e| e.to_string()),
+ }
+}
+
/// A character from its TOML spec, or the built-in buddy. A spec that doesn't
-/// parse falls back rather than failing: the editor should keep drawing while
-/// someone is halfway through breaking their own file.
+/// parse falls back rather than failing — callers who care whether that
+/// happened should ask [`check_spec`] first, which is what the editor does.
fn character_of(toml: Option<&str>) -> Character {
toml.and_then(|t| awan_core::spec::parse(t).ok())
.and_then(|s| Character::from_spec(&s).ok())
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 5bb68de..47bc7bb 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -10,6 +10,9 @@ import { GithubMark } from "./ui/GithubMark";
import { StepIdentity } from "./steps/StepIdentity";
import { StepStory } from "./steps/StepStory";
import { StepExport } from "./steps/StepExport";
+import { Studio, slug } from "./character/Studio";
+import { starter, } from "./character/starters";
+import { toToml, type Character } from "./lib/spec";
/** The shell: which step you're on, and the two pieces of state the steps
* share. Everything that draws lives somewhere else. */
@@ -22,6 +25,15 @@ export function App() {
const [cast, setCast] = useDraft("cast", "awan");
const [beat, setBeat] = useState(-1);
const [solo, setSolo] = useState(-1);
+ // a character you drew, kept with the draft — nobody should lose a drawing to
+ // a reload either
+ const [mine, setMine] = useDraft("mine", null);
+ const [drawing, setDrawing] = useState(false);
+
+ const drawn = mine
+ ? { id: "mine", label: mine.name || "Yours", blurb: mine.description || "drawn by you",
+ toml: toToml(mine), path: `characters/${slug(mine.name)}.toml` }
+ : undefined;
return (
@@ -29,6 +41,10 @@ export function App() {
+ {drawing && mine ? (
+ setDrawing(false)} />
+ ) : (
+ <>
{at === 0 && }
{at === 1 && (
{
+ if (!mine) setMine(starter("blob"));
+ setCast("mine");
+ setDrawing(true);
+ }}
/>
)}
- {at === 2 && }
+ {at === 2 && }
+ >
+ )}
{/* Neither arrow appears where it has nowhere to go. A button whose only
job is to be greyed out is furniture, and the last step's Next was
worse than furniture: it implied a fourth step that doesn't exist. */}
-