feat: setup wizard shell

This commit is contained in:
2026-04-22 12:41:33 +01:00
parent e8005f3fbf
commit aa28a03e3c
5 changed files with 143 additions and 22 deletions

View File

@@ -10,3 +10,4 @@ paste = "1.0"
reqwest = "0.13.2"
serde = "1.0.228"
serde_json = "1.0.149"
tokio = { version = "1.52.1", features = ["rt-multi-thread", "net", "time"] }

View File

@@ -3,10 +3,10 @@ use serde::Deserialize;
use crate::{api, query};
#[derive(Clone)]
struct CreateDeviceCode;
pub struct CreateDeviceCode;
#[derive(Deserialize)]
struct DeviceCodeResponse {
pub struct DeviceCodeResponse {
device_code: String,
user_code: String,
vertification_uri: String,
@@ -20,7 +20,7 @@ impl query::QueryFn for CreateDeviceCode {
type Context = api::QueryContext;
fn key(&self) -> &'static str {
todo!()
"auth.device_code"
}
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {

View File

@@ -1,5 +1,7 @@
use gpui::{bounds, point, prelude::*, px, size};
use crate::screen::welcome;
mod api;
mod app;
mod asset;
@@ -12,6 +14,14 @@ mod theme;
mod titlebar;
fn main() {
// GPUI polls our async query futures, but reqwest relies on Tokio's
// reactor and blocking pool for DNS, sockets, and timers.
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("failed to build Tokio runtime");
let _runtime_guard = runtime.enter();
gpui::Application::new()
.with_assets(asset::Asset)
.run(setup_application);
@@ -48,9 +58,10 @@ fn setup_application(cx: &mut gpui::App) {
traffic_light_position: Some(top_left + point(px(12.), px(12.))),
..Default::default()
}),
is_resizable: false,
..Default::default()
},
|window, cx| cx.new(|cx| app::Chrome::new(window, cx)),
|window, cx| cx.new(|cx| welcome::Screen::new(cx)),
)
.unwrap();
}

View File

@@ -45,20 +45,29 @@ where
T: 'static,
Store<F::Context>: gpui::Global,
{
let ent = cx
.update_global::<Store<F::Context>, _>(|store, cx| store.ensure_query_data(&query_fn, cx));
let ent = cx.update_global::<Store<F::Context>, _>(|store, cx| {
let ent = store.entity_for(&query_fn, cx);
store.ensure_query_data(&query_fn, cx);
ent
});
cx.observe(&ent.raw, |_, _, cx| {
cx.observe(&ent.raw, move |_, ent, cx| {
let query = ent.read(cx);
if matches!(query.data, QueryData::Stale) {
cx.update_global::<Store<F::Context>, _>(|store, cx| {
store.ensure_query_data(&query_fn, cx);
});
}
cx.notify();
})
.detach();
let cloned_ent = ent.clone();
let query_context = cx.global::<Store<F::Context>>().query_context.clone();
cx.observe(&query_context, move |_, _, cx| {
cx.update_global::<Store<F::Context>, _>(|store, cx| {
store.invalidate_query(&query_fn, cx);
store.ensure_query_data(&query_fn, cx);
})
store.invalidate_query(&cloned_ent, cx);
});
})
.detach();
@@ -73,23 +82,41 @@ where
{
let ent = cx.update_global::<Store<F::Context>, _>(|store, cx| store.entity_for(&query_fn, cx));
cx.observe(&ent.raw, |_, ent, cx| {
cx.observe(&ent.raw, move |_, ent, cx| {
let query = ent.read(cx);
if matches!(query.data, QueryData::Stale) {
cx.update_global::<Store<F::Context>, _>(|store, cx| {
store.ensure_query_data(&query_fn, cx);
});
}
cx.notify();
})
.detach();
let cloned_ent = ent.clone();
let query_context = cx.global::<Store<F::Context>>().query_context.clone();
cx.observe(&query_context, move |_, _, cx| {
cx.update_global::<Store<F::Context>, _>(|store, cx| {
store.invalidate_query(&query_fn, cx);
store.ensure_query_data(&query_fn, cx);
})
store.invalidate_query(&cloned_ent, cx);
});
})
.detach();
ent
}
impl<F> Entity<F>
where
F: QueryFn,
Store<F::Context>: gpui::Global,
{
pub fn refetch(&self, cx: &mut gpui::Context<F::Context>) {
cx.update_global::<Store<F::Context>, _>(|store, cx| {
store.invalidate_query(self, cx);
});
}
}
pub fn read_query<'a, F, T>(
query: &Entity<F>,
cx: &'a gpui::Context<T>,
@@ -125,8 +152,8 @@ where
{
pub fn new(ctx: C, cx: &mut gpui::App) -> Self {
Self {
query_data: std::collections::HashMap::new(),
query_context: cx.new(|_| ctx),
query_data: std::collections::HashMap::new(),
}
}
@@ -205,12 +232,13 @@ where
})
}
fn invalidate_query<Q, T>(&mut self, query: &Q, cx: &mut gpui::Context<T>)
fn invalidate_query<E, F>(&self, entity: &Entity<F>, cx: &mut gpui::Context<E>)
where
Q: QueryFn<Context = C>,
T: 'static,
E: 'static,
F: QueryFn<Context = C>,
{
if let Some(entity) = self.query_data.get(query.key()) {
let entity = entity.raw.read(cx);
if let Some(entity) = self.query_data.get(entity.key) {
entity.update(cx, |query, cx| {
if !matches!(query.data, QueryData::Loading) {
query.data = QueryData::Stale;

View File

@@ -1,11 +1,92 @@
struct Welcome {}
use gpui::{FontWeight, ParentElement, Styled, div, px};
impl gpui::Render for Welcome {
use crate::{
api, app,
component::{
font_icon::{FontIcon, font_icon},
text::text,
},
query::{self, use_lazy_query, use_query},
};
pub(crate) struct Screen {
create_device_code_query: query::Entity<api::auth::CreateDeviceCode>,
}
impl Screen {
pub fn new(cx: &mut gpui::Context<Self>) -> Self {
Self {
create_device_code_query: use_lazy_query(api::auth::CreateDeviceCode, cx),
}
}
}
impl gpui::Render for Screen {
fn render(
&mut self,
window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
todo!()
let theme = app::current_theme(cx);
div()
.flex()
.flex_row()
.items_center()
.justify_center()
.size_full()
.child(
div()
.flex()
.items_center()
.justify_center()
.w_1_3()
.h_full()
.bg(theme.colors.surface)
.relative()
.child(
text("Novem", cx)
.font_weight(FontWeight(700.))
.absolute()
.top_20()
.left_8(),
)
.child(step_list(cx)),
)
.child(
div()
.flex()
.flex_col()
.w_2_3()
.px_8()
.h_full()
.bg(theme.colors.background)
.items_start()
.justify_center()
.child(
text(
"Welcome to Novem!\nThis wizard will guide you through setting up Novem.\n",
cx,
)
.opacity(0.8),
)
.child(text("Press 'Next' to begin setup.", cx).font_weight(FontWeight(500.)))
)
}
}
fn step_list(cx: &gpui::Context<impl gpui::Render>) -> impl gpui::IntoElement {
div()
.flex()
.flex_col()
.items_start()
.w_full()
.px_8()
.justify_center()
.gap_3()
.children(vec![
text("Welcome!", cx),
text("Connect to GitHub", cx),
text("Customize Novem", cx),
text("Complete!", cx),
])
}