Warning
The public API is intended to be stable for real use, but future releases may still refine interfaces or introduce compatibility-affecting changes where needed.
TEA-style runtime primitives for Rust developers building desktop applications with GPUI.
gpui_tea is a Rust library for building Elm Architecture applications on top of
GPUI. You use it when you want a
mounted program with explicit state transitions, message-driven updates, and rendering that stays
inside GPUI's application model.
The crate is aimed at developers building desktop user interfaces with GPUI who want a structured
way to express initialization, synchronous updates, asynchronous effects, and long-lived event
sources. The public surface centers on Model, Program, Command, and Subscription, with
support for nested models through ChildScope and the Composite derive macro.
- TEA-style runtime for GPUI with a
Modeltrait that separatesinit,update,view, andsubscriptions. - Command system for immediate messages, foreground effects, background effects, batching, and keyed latest-wins work whose stale completions are ignored.
- Declarative subscriptions that are retained, rebuilt, or removed by stable
Keyvalues. - Nested model support through
ModelContext,ChildScope, and#[derive(Composite)]. - Runtime observability through
ProgramConfig,RuntimeEvent, andTelemetryEvent, with optional adapters fortracingandmetrics.
- Rust stable toolchain. The repository pins the
stablechannel inrust-toolchain.toml. - A Cargo toolchain that supports Rust 2024 edition crates. The manifest does not declare a
separate
rust-version. - For local development in this repository:
clippy,rustfmt, andtypos. - For running the interactive examples: a desktop environment capable of opening GPUI windows.
The repository does not document additional external services such as databases, brokers, or servers.
Add the crate from crates.io:
cargo add gpui_teaEnable optional telemetry integrations as needed:
cargo add gpui_tea --features tracing
cargo add gpui_tea --features metricsTo depend on the current repository state instead of a crates.io release, use a Git dependency:
[dependencies]
gpui_tea = { git = "https://github.com/inkwadra/gpui-tea" }To build the workspace test suite from source:
git clone https://github.com/inkwadra/gpui-tea
cd gpui-tea
cargo test --workspace --all-targets --all-featuresTo run the repository's full validation gate, use:
just qagpui_tea does not use configuration files or required environment variables for normal library
use.
| Feature | Default | Description |
|---|---|---|
metrics |
No | Enables observe_metrics_telemetry. |
tracing |
No | Enables observe_tracing_telemetry and the telemetry example. |
Use ProgramConfig when you need queue controls or observability hooks:
queue_policy(QueuePolicy)selects unbounded, reject-new, drop-newest, or drop-oldest backpressure behavior. Under drop policies,dispatch()may still returnOk(())even when a message is discarded or an older queued message is displaced.queue_warning_threshold(usize)emits queue warning events whenever the current queue depth is greater than the threshold.observer(...)receives high-levelRuntimeEventvalues.telemetry_observer(...)receives structuredTelemetryEnvelopevalues.describe_message(...),describe_key(...), anddescribe_program(...)attach readable descriptions to observability output.
The only environment variable referenced in repository examples is RUST_LOG=debug, which is used
when running the telemetry example.
The usual flow is:
- Define a message enum for your model.
- Implement
Modelfor your state type. - Return
Commandvalues frominitorupdatefor follow-up work. - Mount the model with
Program::mount(...)orModelExt::into_program(...).
The smallest working shape looks like this:
use gpui::{App, Application, Bounds, Window, WindowBounds, WindowOptions, div, px, size};
use gpui::prelude::*;
use gpui_tea::{Command, Dispatcher, IntoView, Model, ModelContext, Program, View};
#[derive(Clone, Copy)]
enum Msg {
Loaded,
}
struct Counter {
value: i32,
}
impl Model for Counter {
type Msg = Msg;
fn init(&mut self, _cx: &mut App, _scope: &ModelContext<Self::Msg>) -> Command<Self::Msg> {
Command::emit(Msg::Loaded)
}
fn update(
&mut self,
msg: Self::Msg,
_cx: &mut App,
_scope: &ModelContext<Self::Msg>,
) -> Command<Self::Msg> {
match msg {
Msg::Loaded => self.value = 1,
}
Command::none()
}
fn view(
&self,
_window: &mut Window,
_cx: &mut App,
_scope: &ModelContext<Self::Msg>,
_dispatcher: &Dispatcher<Self::Msg>,
) -> View {
div().child(format!("count: {}", self.value)).into_view()
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(640.0), px(480.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| Program::mount(Counter { value: 0 }, cx),
)
.unwrap();
cx.activate(true);
});
}- Bootstrap state with
Model::init()and returnCommand::emit(...)or an async command.init()commands use the same queue-drain semantics as commands returned fromupdate(). - Schedule asynchronous work with
Command::foreground(...)orCommand::background(...). - Replace in-flight work by key with
Command::foreground_keyed(...)orCommand::background_keyed(...). Replacing a keyed task requests cancellation of the older task, and any completion that still races in after replacement is ignored as stale. - Cancel tracked keyed work with
Command::cancel_key(...), which requests cancellation for the current task on that key. - Dropping a mounted
Programcancels outstanding async effects that have not completed yet. - Declare long-lived external event sources in
subscriptions()withSubscription::new(...). - Compose child models with
ModelContext::scope(...)or#[derive(Composite)]. Child paths are part of runtime identity, so they must stay stable and unique among siblings.
Signature:
pub trait Model: Sized + 'static {
type Msg: Send + 'static;
fn init(&mut self, cx: &mut App, scope: &ModelContext<Self::Msg>) -> Command<Self::Msg>;
fn update(
&mut self,
msg: Self::Msg,
cx: &mut App,
scope: &ModelContext<Self::Msg>,
) -> Command<Self::Msg>;
fn view(
&self,
window: &mut Window,
cx: &mut App,
scope: &ModelContext<Self::Msg>,
dispatcher: &Dispatcher<Self::Msg>,
) -> View;
fn subscriptions(
&self,
cx: &mut App,
scope: &ModelContext<Self::Msg>,
) -> Subscriptions<Self::Msg>;
}- Parameters:
msgis your domain message,cxis the GPUI application context,scopecontains the current child path, anddispatchersends messages back into the mounted program. - Return type:
Command<Self::Msg>frominitandupdate,Viewfromview,Subscriptions<Self::Msg>fromsubscriptions.
Example:
fn init(&mut self, _cx: &mut App, _scope: &ModelContext<Self::Msg>) -> Command<Self::Msg> {
Command::emit(()).label("bootstrap")
}Signatures:
pub fn mount(model: M, cx: &mut App) -> Entity<Program<M>>;
pub fn mount_with(model: M, config: ProgramConfig<M::Msg>, cx: &mut App) -> Entity<Program<M>>;- Parameters:
modelis the initial state,configcustomizes queue and observability behavior, andcxis the GPUI application context. - Behavior: mounting immediately calls
Model::init(), executes its returned command through the normal queue-drain model, drains all causally enqueued synchronous init messages, and then performs the initial subscription reconciliation before returning. - Return type:
Entity<Program<M>>.
Example:
let config = ProgramConfig::<Msg>::new().queue_warning_threshold(32);
let entity = Program::mount_with(Counter { value: 0 }, config, cx);Representative constructors:
pub fn none() -> Command<Msg>;
pub fn emit(message: Msg) -> Command<Msg>;
pub fn batch(commands: impl IntoIterator<Item = Command<Msg>>) -> Command<Msg>;
pub fn foreground<AsyncFn>(effect: AsyncFn) -> Command<Msg>;
pub fn background<F, Fut>(effect: F) -> Command<Msg>;
pub fn foreground_keyed<AsyncFn>(key: impl Into<Key>, effect: AsyncFn) -> Command<Msg>;
pub fn background_keyed<F, Fut>(key: impl Into<Key>, effect: F) -> Command<Msg>;
pub fn cancel_key(key: impl Into<Key>) -> Command<Msg>;
pub fn map<F, NewMsg>(self, f: F) -> Command<NewMsg>;- Parameters: commands take either a concrete message, an async effect closure, or a stable
Keyused for deduplication and cancellation. - Keyed commands are latest-wins: scheduling a newer keyed command replaces the tracked task for that key and requests cancellation of the older task. If the older task still completes in a race, the runtime ignores that stale completion.
- Non-keyed async commands remain owned by the mounted
Programuntil they complete or the program is dropped. - Return type:
Command<Msg>orCommand<NewMsg>formap.
Example:
Command::background_keyed("load-profile", |_| async move {
Some(())
})
.label("profile-load")Signatures:
pub fn new<F>(key: impl Into<Key>, builder: F) -> Subscription<Msg>;
pub fn one(subscription: Subscription<Msg>) -> Subscriptions<Msg>;
pub fn batch(
subscriptions: impl IntoIterator<Item = Subscription<Msg>>,
) -> Result<Subscriptions<Msg>>;- Parameters:
keyis stable subscription identity, andbuilderreceives aSubscriptionContext<'_, Msg>with access toAppand the programDispatcher. - Constraint: keys must be unique within a
Subscriptionsset.Subscriptions::batch(...)andpush(...)returnError::DuplicateSubscriptionKeywhen duplicates are declared. - Return type:
Subscription<Msg>orSubscriptions<Msg>.
Example:
Subscriptions::<()>::one(
Subscription::new("clock", |cx| {
cx.dispatch(()).expect("program should be mounted");
gpui_tea::SubHandle::None
})
.label("clock-subscription"),
)Syntax:
#[derive(Composite)]
#[composite(message = ParentMsg)]
struct Parent {
#[child(path = "sidebar", lift = ParentMsg::Sidebar, extract = ParentMsg::into_sidebar)]
sidebar: SidebarModel,
}- Parameters:
messagedeclares the parent message type; eachchildattribute defines a stable string-literal path segment, the lift function, and the extractor used to route parent messages back to the child. Sibling child paths must be unique within the derive target. - Generated helpers: the macro adds hidden aggregate methods
__composite_init,__composite_update, and__composite_subscriptions, plus one hidden<field>_viewhelper per child field. - Manual
ModelContext::scope(...)remains more flexible, but the caller is responsible for choosing path segments that remain stable and unique for the child lifecycle.
Example:
fn init(&mut self, cx: &mut App, scope: &ModelContext<Self::Msg>) -> Command<Self::Msg> {
self.__composite_init(cx, scope)
}Run the packaged examples from the workspace root:
cargo run -p gpui_tea --example counter
cargo run -p gpui_tea --example init_command
cargo run -p gpui_tea --example keyed_effect
cargo run -p gpui_tea --example nested_models
cargo run -p gpui_tea --example subscriptions
cargo run -p gpui_tea --example observability
RUST_LOG=debug cargo run -p gpui_tea --example telemetry --features tracingEach example focuses on one runtime behavior:
counter: minimal mounted program and message dispatch from the view.init_command: bootstrap work triggered byModel::init().keyed_effect: latest-wins async work on a stable key.nested_models:Compositecomposition with stable child path segments.subscriptions: declarative subscription reconciliation by key.observability:RuntimeEventhooks with readable labels.telemetry: structured tracing output for queue activity, keyed replacement, cancellation, and stale-completion races.
For repository development, the Justfile mirrors CI:
just fmt
just fmt-check
just check
just clippy
just lint
just typos
just doc
just test
just qa
just fixLicensed under Apache-2.0.