SDKs

Write Lumen apps from Rust, C++, Python — or any C-capable language.

The markup, CSS, and Rhai stay the UI. An SDK lets your host language own the state and the event handlers instead — driving the same runtime, with same-tick reactivity, from whichever language your app already lives in.

Rust — the lumen crate C++ — lumen.hpp Python — lumen-ui C — the ABI itself
Rust

The lumen crate

Lumen runs on bevy_ecs, and the crate exposes it in bevy's own shape: add plugin groups, add real systems, run. Your handlers take Query, Res, MessageReader — nothing is stringly-typed.

bevy_ecs systemsplugin groupstyped signalsforbid(unsafe_code)
add the dependency
cargo add lumen
  • Typed signals via the Signals system param — signals.get_or::<i64>("count", 0) / set; the signals! macro mints typed handle structs so a name is spelled once.
  • Real bevy_ecs systems scheduled into the live tick — read MessageReader<ClickEvent>, mutate TextContent through a query, chain with .chain().
  • Run conditions gate systems declaratively — reset.run_if(on_click("reset")), plus on_toggle and on_change.
  • Decomposable plugin groups — LumenDefaultPlugins is the whole stack; .build().disable::<ScriptPlugin>() yields a pure-Rust app with no script host.
counter.rs
use lumen::prelude::*;

fn main() -> lumen::Result<()> {
    lumen::App::new()
        .add_plugins(
            LumenDefaultPlugins
                .with_markup(include_str!("counter.lmn"))
                .with_css(include_str!("counter.css"))
                .with_title("Lumen counter"),
        )
        .insert_signal("count", 0i64)
        .add_systems(TickStage::Systems, (bump, label).chain())
        .run()
}

fn bump(mut clicks: MessageReader<ClickEvent>, mut signals: Signals) {
    let hits = clicks.read().count() as i64;
    if hits > 0 {
        let n = signals.get_or::<i64>("count", 0) + hits;
        signals.set("count", n);
    }
}

fn label(signals: Signals, mut q: Query<(&LumenId, &mut TextContent)>) {
    let n = signals.get_or::<i64>("count", 0);
    for (id, mut text) in &mut q {
        if id.0 == "counter-label" {
            text.0 = format!("clicks: {n}");
        }
    }
}
C++

Header-only lumen.hpp

One header, C++17, no third-party dependencies. An RAII lumen::App that verifies the ABI on construction, typed lumen::Signal<T> handles safe from any thread, and std::function callbacks bridged into the runtime.

header-onlyC++17Signal<T>RAII
CMakeLists.txt — or pkg-config
find_package(lumen-cpp CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE lumen::cpp)
  • Typed Signal<T> handles for int64_t, double, bool, std::string, and Colorcount += 1 writes, *count reads, safe from any thread.
  • signal.watch(fn) — a real ABI subscription that fires when the value commits, not a polling loop.
  • Id-scoped app.on_click(id, fn) taking void() or void(std::string); run_checked() throws lumen::Error on failure, run_headless(ticks) covers CI.
  • ABI-checked App — the constructor throws lumen::Error on a header/library version mismatch.
main.cpp
#include <lumen.hpp>

lumen::Signal<int64_t>     count{"count", 0};
lumen::Signal<std::string> label{"label", "0 clicks"};

int main() {
    lumen::App app("counter_app",
                   {.title = "Counter", .width = 480, .height = 280});

    app.on_click("increment", [] {
        count += 1;
        label = std::to_string(*count) + " clicks";
    });

    count.watch([](int64_t n) {
        // fires on the tick each new value commits
    });

    app.run_checked();
}
Python

Stdlib-only lumen-ui

Pure ctypes — no compiled extension, no build step of its own. Subclass lumen.Model and every annotated field is a typed reactive signal: state.count += 1 is a runtime write.

ctypesstdlib onlylumen.ModelPython 3.9+
shell
pip install lumen-ui
  • lumen.Model — annotated fields become typed signals with IDE autocompletion; reading a field reads the signal, assigning writes it.
  • Signal handles — .value reads and writes, .watch(fn) subscribes on commit, lumen.computed derives one signal from others.
  • @app.on_click / on_double_click / on_long_press decorators; App is a context manager that frees the handle for you.
  • Headless app.run_headless(ticks) for CI, no display required; a LumenError hierarchy with one exception class per ABI status code.
counter.py
import lumen

class Counter(lumen.Model):
    count: int = 0
    label: str = "0 clicks"

app = lumen.App("counter_app", title="Counter", size=(480, 280))
state = Counter(app)

@app.on_click("increment")
def _increment(_: str) -> None:
    state.count += 1
    state.label = f"{state.count} clicks"

with app:
    app.run()  # blocks until the window closes
C

The ABI everything builds on

Under the three SDKs is one hand-written C ABI, lumen.h (version 0.4). It is the raw foundation — link the shared library, include one header, and drive Lumen from any language that speaks C. The same stringly layer also surfaces inside the SDKs as lumen.raw (Python) and lumen::raw (C++) for the rare call the typed surface doesn't cover.

lumen.hABI 0.4liblumen_ffipanic-safe
shell
cc counter.c -I lumen/ffi/include -llumen_ffi -o counter
  • Typed scalar signals — lumen_signal_set_int64 / _get_int64, _float64, _bool, _color — plus lumen_signal_watch change subscriptions.
  • Id-scoped native clicks — lumen_app_on_click; headless ticks via lumen_app_run_headless.
  • Panic-safe boundary — a Rust panic at the edge becomes LUMEN_ERR_PANIC; it never unwinds into your frames.
  • ABI-versioned — compare lumen_abi_version() against LUMEN_API_VERSION; the major+minor pair must match.
counter.c
#include <lumen.h>

static void on_increment(const char *id, void *user) {
    int64_t n = 0;
    lumen_signal_get_int64(NULL, "count", &n);
    lumen_signal_set_int64(NULL, "count", n + 1);
}

int main(void) {
    if ((lumen_abi_version() >> 8) != (LUMEN_API_VERSION >> 8))
        return 1;   /* header vs library ABI mismatch */

    lumen_signal_set_int64(NULL, "count", 0);

    LumenApp *app = lumen_app_new("path/to/app");
    lumen_app_set_title(app, "Counter");
    lumen_app_on_click(app, "increment", on_increment, NULL);
    return lumen_app_run(app);
}
How it fits

One runtime, every language.

Markup, CSS, and Rhai still describe the view. An SDK just moves the signals and handlers into your host language — reads and writes land on the same PropertyStore, on the same tick, so a value set from Rust, C++, Python, or C reaches bound markup the very frame it changes.

Browse the reference →
# the UI — shared across every SDK main.lmn markup + bind-text main.css CSS-subset styling # the host — pick one Rust lumen (cargo add) C++ lumen.hpp (lumen::cpp) Python lumen-ui (pip) C lumen.h (ABI 0.4)