From aa28a03e3c4ae83b30efd761a41b95e008c01fec Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 22 Apr 2026 12:41:33 +0100 Subject: [PATCH] feat: setup wizard shell --- Cargo.toml | 1 + src/api/auth.rs | 6 +-- src/main.rs | 13 ++++++- src/query.rs | 58 +++++++++++++++++++++-------- src/screen/welcome.rs | 87 +++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 143 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0ee421b..edb68f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/src/api/auth.rs b/src/api/auth.rs index cf88845..0423974 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -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 { diff --git a/src/main.rs b/src/main.rs index 579ffda..b10dae4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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(); } diff --git a/src/query.rs b/src/query.rs index d989094..6581c0b 100644 --- a/src/query.rs +++ b/src/query.rs @@ -45,20 +45,29 @@ where T: 'static, Store: gpui::Global, { - let ent = cx - .update_global::, _>(|store, cx| store.ensure_query_data(&query_fn, cx)); + let ent = cx.update_global::, _>(|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, cx| { + store.ensure_query_data(&query_fn, cx); + }); + } cx.notify(); }) .detach(); + let cloned_ent = ent.clone(); let query_context = cx.global::>().query_context.clone(); cx.observe(&query_context, move |_, _, cx| { cx.update_global::, _>(|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, 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, cx| { + store.ensure_query_data(&query_fn, cx); + }); + } cx.notify(); }) .detach(); + let cloned_ent = ent.clone(); let query_context = cx.global::>().query_context.clone(); cx.observe(&query_context, move |_, _, cx| { cx.update_global::, _>(|store, cx| { - store.invalidate_query(&query_fn, cx); - store.ensure_query_data(&query_fn, cx); - }) + store.invalidate_query(&cloned_ent, cx); + }); }) .detach(); ent } +impl Entity +where + F: QueryFn, + Store: gpui::Global, +{ + pub fn refetch(&self, cx: &mut gpui::Context) { + cx.update_global::, _>(|store, cx| { + store.invalidate_query(self, cx); + }); + } +} + pub fn read_query<'a, F, T>( query: &Entity, cx: &'a gpui::Context, @@ -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(&mut self, query: &Q, cx: &mut gpui::Context) + fn invalidate_query(&self, entity: &Entity, cx: &mut gpui::Context) where - Q: QueryFn, - T: 'static, + E: 'static, + F: QueryFn, { - 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; diff --git a/src/screen/welcome.rs b/src/screen/welcome.rs index c315511..3aedbc9 100644 --- a/src/screen/welcome.rs +++ b/src/screen/welcome.rs @@ -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, +} + +impl Screen { + pub fn new(cx: &mut gpui::Context) -> 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, ) -> 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::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), + ]) +}