inkwadra/gpui-tea
TEA-style runtime primitives for Rust developers building desktop applications with GPUI.
gpui_tea
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.
Table of Contents
Features
- TEA-style runtime for GPUI with a
Modeltrait that separatesinit,update,view, and
subscriptions. - 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.
Requirements
- 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
separaterust-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.
Installation
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 qaConfiguration
gpui_tea does not use configuration files or required environment variables for normal library
use.
Cargo Features
| Feature | Default | Description |
|---|---|---|
metrics |
No | Enables observe_metrics_telemetry. |
tracing |
No | Enables observe_tracing_telemetry and the telemetry example. |
Runtime Configuration
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.
Usage
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);
});
}Common Patterns
- 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(...)or
Command::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.
API Reference
Model
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,scope
contains 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")
}Program::mount And Program::mount_with
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);Command
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
Key
used 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")Subscription And Subscriptions
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 a
SubscriptionContext<'_, Msg>with access toAppand the programDispatcher. - Constraint: keys must be unique within a
Subscriptionsset.Subscriptions::batch(...)and
push(...)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"),
)#[derive(Composite)]
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)
}Examples
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 fixLicense
Licensed under Apache-2.0.