8000 GitHub - inkwadra/gpui-tea: TEA-style runtime primitives for Rust developers building desktop applications with GPUI. · GitHub
[go: up one dir, main page]

Skip to content
inkwadraPublic

About

TEA-style runtime primitives for Rust developers building desktop applications with GPUI.

Topics

Resources

Stars

12 stars

Watchers

1 watching

Forks

Repository files navigation

gpui_tea

CI Crates.io docs.rs License

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 Model trait that separates init, 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 Key values.
  • Nested model support through ModelContext, ChildScope, and #[derive(Composite)].
  • Runtime observability through ProgramConfig, RuntimeEvent, and TelemetryEvent, with optional adapters for tracing and metrics.

Requirements

  • Rust stable toolchain. The repository pins the stable channel in rust-toolchain.toml.
  • Rust 1.98 or newer. The workspace declares rust-version = "1.98", following the toolchain that Zed uses to build GPUI.
  • GPUI from the Zed repository (https://github.com/zed-industries/zed). gpui_tea tracks Zed's default branch rather than the gpui releases on crates.io.
  • For local development in this repository: clippy, rustfmt, and typos.
  • 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

gpui_tea depends on GPUI from Zed's Git repository. Add it together with GPUI and gpui_platform, which provides the platform backend that application() starts:

[dependencies]
gpui = { git = "https://github.com/zed-industries/zed" }
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "wayland", "x11"] }
gpui_tea = { git = "https://github.com/inkwadra/gpui-tea" }

Enable optional telemetry integrations as needed:

gpui_tea = { git = "https://github.com/inkwadra/gpui-tea", features = ["tracing", "metrics"] }

Cargo only unifies Git dependencies that use the same URL and reference. Declare gpui exactly as above (no branch, tag, or rev) so your application and gpui_tea share one GPUI build; the exact commit is pinned by your Cargo.lock. If you pin GPUI to a specific revision, redirect gpui_tea's GPUI to it with a patch:

[patch."https://github.com/zed-industries/zed"]
gpui = { git = "https://github.com/zed-industries/zed", rev = "<commit>" }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "<commit>" }

The 0.1.x releases on crates.io are built against gpui 0.2.2 from crates.io and do not include the changes on the default branch.

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-features

To run the repository's full validation gate, use:

just qa

Configuration

gpui_tea does not use configuration files or required environment variables for normal library use.

Cargo Features

Feature Default Description
metrics No Enables MetricsAdapter, MetricsAdapterConfig, and 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 for external dispatches. Under drop policies, dispatch() may still return Ok(()) even when a message is discarded or an older queued message is displaced. Displaced messages are released immediately, so bounded policies also bound memory.
  • queue_warning_threshold(usize) emits queue warning events whenever the queue depth (accepted external and internal messages that are not processed yet) is greater than the threshold. This is the same depth reported by RuntimeSnapshot::queue_depth and telemetry metadata.
  • drain_budget(usize) caps how many messages one queue drain processes before yielding to the GPUI executor (default 1024), so long emit chains cannot freeze rendering.
  • observer(...) receives high-level RuntimeEvent values.
  • telemetry_observer(...) receives structured TelemetryEnvelope values.
  • describe_message(...), describe_key(...), and describe_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:

  1. Define a message enum for your model.
  2. Implement Model for your state type.
  3. Return Command values from init or update for follow-up work.
  4. Mount the model with Program::mount(...) or ModelExt::into_program(...).

The smallest working shape looks like this:

use gpui::{App, Bounds, Window, WindowBounds, WindowOptions, div, px, size};
use gpui::prelude::*;
use gpui_platform::application;
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().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 return Command::emit(...) or an async command. init() commands use the same queue-drain semantics as commands returned from update().
  • Schedule asynchronous work with Command::foreground(...) or Command::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 Program cancels outstanding async effects that have not completed yet.
  • Declare long-lived external event sources in subscriptions() with Subscription::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: msg is your domain message, cx is the GPUI application context, scope contains the current child path, and dispatcher sends messages back into the mounted program.
  • Return type: Command<Self::Msg> from init and update, View from view, Subscriptions<Self::Msg> from subscriptions.

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: model is the initial state, config customizes queue and observability behavior, and cx is the GPUI application context.
  • Behavior: mounting immediately calls Model::init(), executes its returned command through the normal queue-drain model, drains causally enqueued synchronous init messages up to the drain budget, and then performs the initial subscription reconciliation before returning.
  • Externally dispatched messages are delivered in batches: everything waiting when the program wakes up is processed in one drain, followed by one subscription reconciliation and one view notification.
  • Panics in Model callbacks are fatal: GPUI leaves the panicking program entity unusable.
  • 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<<
A3B9
/span>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 Program until they complete or the program is dropped.
  • Return type: Command<Msg> or Command<NewMsg> for map.

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: key is stable subscription identity, and builder receives a SubscriptionContext<'_, Msg> with access to App and the program Dispatcher.
  • Constraint: keys must be unique within a Subscriptions set. Subscriptions::batch(...) and push(...) return Error::DuplicateSubscriptionKey when duplicates are declared.
  • Return type: Subscription<Msg> or Subscriptions<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: message declares the parent message type; each child attribute 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>_view helper per child field. The helpers share the struct's visibility, so the Model implementation may live in another module, and the generated code does not require a direct gpui dependency.
  • 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 tracing

Each example focuses on one runtime behavior:

  • counter: minimal mounted program and message dispatch from the view.
  • init_command: bootstrap work triggered by Model::init().
  • keyed_effect: latest-wins async work on a stable key.
  • nested_models: Composite composition with stable child path segments.
  • subscriptions: declarative subscription reconciliation by key.
  • observability: RuntimeEvent hooks 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 fix

License

Licensed under Apache-2.0.

About

TEA-style runtime primitives for Rust developers building desktop applications with GPUI.

Topics

Resources

Stars

12 stars

Watchers

1 watching

Forks

Contributors

Languages

0