Writing a Machine

A step-by-step walk from empty type to working machine.

1. Name your four types

#![allow(unused)]
fn main() {
#[derive(Clone)]
struct Ctx { user: String }          // Context — pushed via set_context

#[derive(Clone)]
enum Msg { Submit, Cancel }          // Msg — host/machine vocabulary

#[derive(Clone, Default)]
struct Form { buffer: String }       // Model — participant-local

type Shared = ();                    // Shared — host projection (none yet)
}

Model and Shared need IntoGeometric/FromGeometric (re-exported from knopper). Numbers, bool, String, (), tuples, and Option implement it out of the box; for structs, a hash-based impl is fine — the native value is cached and the geometric form is only a change fingerprint.

2. init — initial local state

#![allow(unused)]
fn main() {
|ctx: &Ctx| Form::default()
}

3. update — the only place Model changes

#![allow(unused)]
fn main() {
|model: &mut Form, msg: Msg, _ctx: &Ctx| match msg {
    Msg::Submit => { model.buffer.clear(); Effect::None }
    Msg::Cancel => { model.buffer.clear(); Effect::None }
}
}

Effects are for control flow, not orchestration: Effect::Emit(msg) chains a follow-up message synchronously, Effect::Batch applies several, Effect::RequestFocus(id) moves focus. The runtime drains them completely — see Runtime.

4. project — the scene as a function of state

#![allow(unused)]
fn main() {
|model: &Form, _shared: &(), ctx: &Ctx| {
    Scene::column(1_u64, vec![
        Scene::text(2_u64, format!("hello, {}", ctx.user)),
        Scene::text(3_u64, model.buffer.clone()),
        Scene::text(4_u64, "[ submit ]")
            .focusable()
            .on_activate(Msg::Submit),
    ])
}
}

Use stable ids; the diff is id-keyed. For PureMachine this closure is the view argument; custom Machine impls get the reactive project form with Behavior arguments.

5. Drive it

#![allow(unused)]
fn main() {
let mut runtime = Runtime::new(machine, Ctx { user: "ada".into() }, ());
runtime.send(Msg::Submit);
let mut backend = knopper::MockBackend::default();
runtime.render_to_backend(&mut backend, Rect::new(0, 0, 80, 24)).unwrap();
}

Testing

Machines are plain values — test update directly, and test the runtime path against MockBackend. tests/embedding_harness.rs in the repo is a complete worked example of host-side machine definitions and assertions.