This is a pure rust + GPUI application that allows the user to see the current limits of various AI agent subscriptions that they have. There is a reference implementaion in references/CodexBar. We will implement a pure rust + GPUI port of it.
The port must be a 1:1 match for references/CodexBar in terms of functionality and UI.
/Users/adityasharma/Projects/friday_v2/terminal-app is a good starting point on how to use GPUI and GPUI components.
-
use this specific fork and version of GPUI gpui = { git = "https://github.com/BumpyClock/zed", package = "gpui", rev = "149e1724574f4e27eeeadcc62e1e15e7afeddc25" }
-
For GPUI components, pre-built set of GPUI components use our fork here
https://github.com/BumpyClock/gpui-componentyou can vendor it like the terminal-app does as a git submodule so we can iterate on GPUI components as needed. Check with the user if you need any specific components that are not available in the gpui-component repo.
use the gpui skill for writing correct rust + GPUI code.
- Prioritize code correctness and clarity. Speed and efficiency are secondary priorities unless otherwise specified.
- Do not write organizational or comments that summarize the code. Comments should only be written in order to explain "why" the code is written in some way in the case there is a reason that is tricky / non-obvious.
- Prefer implementing functionality in existing files unless it is a new logical component. Avoid creating many small files.
- Avoid using functions that panic like
unwrap(), instead use mechanisms like?to propagate errors. - Be careful with operations like indexing which may panic if the indexes are out of bounds.
- Never silently discard errors with
let _ =on fallible operations. Always handle errors appropriately:- Propagate errors with
?when the calling function should handle them - Use
.log_err()or similar when you need to ignore errors but want visibility - Use explicit error handling with
matchorif let Err(...)when you need custom logic - Example: avoid
let _ = client.request(...).await?;- useclient.request(...).await?;instead
- Propagate errors with
- When implementing async operations that may fail, ensure errors propagate to the UI layer so users get meaningful feedback.
- Never create files with
mod.rspaths - prefersrc/some_module.rsinstead ofsrc/some_module/mod.rs. - When creating new crates, prefer specifying the library root path in
Cargo.tomlusing[lib] path = "...rs"instead of the defaultlib.rs, to maintain consistent and descriptive naming (e.g.,gpui.rsormain.rs). - Avoid creative additions unless explicitly requested
- Use full words for variable names (no abbreviations like "q" for "queue")
- Use variable shadowing to scope clones in async contexts for clarity, minimizing the lifetime of borrowed references.
Example:
executor.spawn({ let task_ran = task_ran.clone(); async move { *task_ran.borrow_mut() = true; } });
- In GPUI tests, prefer GPUI executor timers over
smol::Timer::after(...)when you need timeouts, delays, or to driverun_until_parked():- Use
cx.background_executor().timer(duration).await(orcx.background_executor.timer(duration).awaitinTestAppContext) so the work is scheduled on GPUI's dispatcher. - Avoid
smol::Timer::after(...)for test timeouts when you rely onrun_until_parked(), because it may not be tracked by GPUI's scheduler and can lead to "nothing left to run" when pumping.
- Use
GPUI is a UI framework which also provides primitives for state and concurrency management.
Context types allow interaction with global state, windows, entities, and system services. They are typically passed to functions as the argument named cx. When a function takes callbacks they come after the cx parameter.
Appis the root context type, providing access to global state and read and update of entities.Context<T>is provided when updating anEntity<T>. This context dereferences intoApp, so functions which take&Appcan also take&Context<T>.AsyncAppandAsyncWindowContextare provided bycx.spawnandcx.spawn_in. These can be held across await points.
Window provides access to the state of an application window. It is passed to functions as an argument named window and comes before cx when present. It is used for managing focus, dispatching actions, directly drawing, getting user input state, etc.
An Entity<T> is a handle to state of type T. With thing: Entity<T>:
thing.entity_id()returnsEntityIdthing.downgrade()returnsWeakEntity<T>thing.read(cx: &App)returns&T.thing.read_with(cx, |thing: &T, cx: &App| ...)returns the closure's return value.thing.update(cx, |thing: &mut T, cx: &mut Context<T>| ...)allows the closure to mutate the state, and provides aContext<T>for interacting with the entity. It returns the closure's return value.thing.update_in(cx, |thing: &mut T, window: &mut Window, cx: &mut Context<T>| ...)takes aAsyncWindowContextorVisualTestContext. It's the same asupdatewhile also providing theWindow.
Within the closures, the inner cx provided to the closure must be used instead of the outer cx to avoid issues with multiple borrows.
Trying to update an entity while it's already being updated must be avoided as this will cause a panic.
When read_with, update, or update_in are used with an async context, the closure's return value is wrapped in an anyhow::Result.
WeakEntity<T> is a weak handle. It has read_with, update, and update_in methods that work the same, but always return an anyhow::Result so that they can fail if the entity no longer exists. This can be useful to avoid memory leaks - if entities have mutually recursive handles to each other they will never be dropped.
All use of entities and UI rendering occurs on a single foreground thread.
cx.spawn(async move |cx| ...) runs an async closure on the foreground thread. Within the closure, cx is &mut AsyncApp.
When the outer cx is a Context<T>, the use of spawn instead looks like cx.spawn(async move |this, cx| ...), where this: WeakEntity<T> and cx: &mut AsyncApp.
To do work on other threads, cx.background_spawn(async move { ... }) is used. Often this background task is awaited on by a foreground task which uses the results to update state.
Both cx.spawn and cx.background_spawn return a Task<R>, which is a future that can be awaited upon. If this task is dropped, then its work is cancelled. To prevent this one of the following must be done:
- Awaiting the task in some other async context.
- Detaching the task via
task.detach()ortask.detach_and_log_err(cx), allowing it to run indefinitely. - Storing the task in a field, if the work should be halted when the struct is dropped.
A task which doesn't do anything but provide a value can be created with Task::ready(value).
The Render trait is used to render some state into an element tree that is laid out using flexbox layout. An Entity<T> where T implements Render is sometimes called a "view".
Example:
struct TextWithBorder(SharedString);
impl Render for TextWithBorder {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().border_1().child(self.0.clone())
}
}
Since impl IntoElement for SharedString exists, it can be used as an argument to child. SharedString is used to avoid copying strings, and is either an &'static str or Arc<str>.
UI components that are constructed just to be turned into elements can instead implement the RenderOnce trait, which is similar to Render, but its render method takes ownership of self and receives &mut App instead of &mut Context<Self>. Types that implement this trait can use #[derive(IntoElement)] to use them directly as children.
The style methods on elements are similar to those used by Tailwind CSS.
If some attributes or children of an element tree are conditional, .when(condition, |this| ...) can be used to run the closure only when condition is true. Similarly, .when_some(option, |this, value| ...) runs the closure when the Option has a value.
Input event handlers can be registered on an element via methods like .on_click(|event, window, cx: &mut App| ...).
Often event handlers will want to update the entity that's in the current Context<T>. The cx.listener method provides this - its use looks like .on_click(cx.listener(|this: &mut T, event, window, cx: &mut Context<T>| ...).
Actions are dispatched via user keyboard interaction or in code via window.dispatch_action(SomeAction.boxed_clone(), cx) or focus_handle.dispatch_action(&SomeAction, window, cx).
Actions with no data defined with the actions!(some_namespace, [SomeAction, AnotherAction]) macro call. Otherwise the Action derive macro is used. Doc comments on actions are displayed to the user.
Action handlers can be registered on an element via the event handler .on_action(|action, window, cx| ...). Like other event handlers, this is often used with cx.listener.
When a view's state has changed in a way that may affect its rendering, it should call cx.notify(). This will cause the view to be rerendered. It will also cause any observe callbacks registered for the entity with cx.observe to be called.
While updating an entity (cx: Context<T>), it can emit an event using cx.emit(event). Entities register which events they can emit by declaring impl EventEmitter<EventType> for EntityType {}.
Other entities can then register a callback to handle these events by doing cx.subscribe(other_entity, |this, other_entity, event, cx| ...). This will return a Subscription which deregisters the callback when dropped. Typically cx.subscribe happens when creating a new entity and the subscriptions are stored in a _subscriptions: Vec<Subscription> field.
- Use
./scripts/clippyinstead ofcargo clippy