From e8005f3fbf6e56bc04f3d2e5c6ef53454c818a60 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 21 Apr 2026 20:30:41 +0100 Subject: [PATCH] wip --- Cargo.toml | 2 + src/api.rs | 25 ++++- src/api/auth.rs | 40 ++++++++ src/api/repo.rs | 5 +- src/api/user.rs | 6 +- src/asset/font_icon/github.svg | 1 + src/component/button.rs | 50 ++++++++-- src/component/font_icon.rs | 3 +- src/main.rs | 4 + src/query.rs | 161 ++++++++++++++++++--------------- src/screen.rs | 1 + src/screen/welcome.rs | 11 +++ src/titlebar.rs | 22 +++-- 13 files changed, 234 insertions(+), 97 deletions(-) create mode 100644 src/api/auth.rs create mode 100644 src/asset/font_icon/github.svg create mode 100644 src/screen.rs create mode 100644 src/screen/welcome.rs diff --git a/Cargo.toml b/Cargo.toml index 9ce4bb5..0ee421b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,5 @@ anyhow = "1" gpui = { version = "*" } paste = "1.0" reqwest = "0.13.2" +serde = "1.0.228" +serde_json = "1.0.149" diff --git a/src/api.rs b/src/api.rs index c4cba3e..d00004e 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,5 +1,6 @@ use crate::query; +pub(crate) mod auth; pub(crate) mod repo; pub(crate) mod user; @@ -7,16 +8,36 @@ pub(crate) mod user; pub struct QueryContext { pub(crate) http: reqwest::Client, pub(crate) auth: Option, + pub(crate) github: GithubCredentials, } #[derive(Clone)] pub(crate) struct Auth { - pub(crate) access_token: String, - pub(crate) refresh_token: String, + pub(crate) access_token: &'static str, + pub(crate) refresh_token: &'static str, +} + +#[derive(Clone)] +pub(crate) struct GithubCredentials { + pub(crate) client_id: &'static str, } pub enum Error { Unauthenticated, + MalformedResponse(serde_json::Error), + HttpError(reqwest::Error), } impl query::Context for QueryContext {} + +impl From for Error { + fn from(value: reqwest::Error) -> Self { + Self::HttpError(value) + } +} + +impl From for Error { + fn from(value: serde_json::Error) -> Self { + Self::MalformedResponse(value) + } +} diff --git a/src/api/auth.rs b/src/api/auth.rs new file mode 100644 index 0000000..cf88845 --- /dev/null +++ b/src/api/auth.rs @@ -0,0 +1,40 @@ +use serde::Deserialize; + +use crate::{api, query}; + +#[derive(Clone)] +struct CreateDeviceCode; + +#[derive(Deserialize)] +struct DeviceCodeResponse { + device_code: String, + user_code: String, + vertification_uri: String, + expires_in: u16, + interval: u16, +} + +impl query::QueryFn for CreateDeviceCode { + type Data = DeviceCodeResponse; + type Error = api::Error; + type Context = api::QueryContext; + + fn key(&self) -> &'static str { + todo!() + } + + async fn run(&self, c: &Self::Context) -> Result { + let data = c + .http + .post(format!( + "https://github.com/login/device/code?client_id={}", + c.github.client_id + )) + .send() + .await? + .bytes() + .await?; + + serde_json::from_slice::(&data).map_err(|e| e.into()) + } +} diff --git a/src/api/repo.rs b/src/api/repo.rs index d4888d0..f8adf9d 100644 --- a/src/api/repo.rs +++ b/src/api/repo.rs @@ -4,15 +4,16 @@ use crate::query; #[derive(Clone)] pub struct List; -impl query::QueryFn for List { +impl query::QueryFn for List { type Data = (); type Error = (); + type Context = QueryContext; fn key(&self) -> &'static str { "repo.list" } - async fn run(&self, _c: &QueryContext) -> Result { + async fn run(&self, _c: &Self::Context) -> Result { Ok(()) } } diff --git a/src/api/user.rs b/src/api/user.rs index 714f79f..82ee605 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -3,16 +3,16 @@ use crate::{api, query}; #[derive(Clone)] pub struct Fetch; -impl query::QueryFn for Fetch { +impl query::QueryFn for Fetch { type Data = api::Error; - type Error = api::Error; + type Context = api::QueryContext; fn key(&self) -> &'static str { "user" } - async fn run(&self, c: &api::QueryContext) -> Result { + async fn run(&self, c: &Self::Context) -> Result { Err(api::Error::Unauthenticated) } } diff --git a/src/asset/font_icon/github.svg b/src/asset/font_icon/github.svg new file mode 100644 index 0000000..2334976 --- /dev/null +++ b/src/asset/font_icon/github.svg @@ -0,0 +1 @@ +GitHub diff --git a/src/component/button.rs b/src/component/button.rs index 2b72241..70dd347 100644 --- a/src/component/button.rs +++ b/src/component/button.rs @@ -1,14 +1,24 @@ -use gpui::{FontWeight, InteractiveElement, ParentElement, Styled, div}; +use gpui::{AnyElement, FontWeight, InteractiveElement, ParentElement, Styled, div}; use crate::{app, component::text::Text}; -pub struct Button(gpui::Stateful); +pub struct Button { + div: gpui::Stateful, + text_color: gpui::Rgba, + label: Option, + leading: Option, + trailing: Option, +} -pub fn button(id: impl Into, cx: &gpui::Context) -> Button { +pub fn button(id: impl Into, cx: &gpui::Context) -> Button { let theme = app::current_theme(cx); - Button( - div() + Button { + div: div() .id(id) + .flex() + .flex_row() + .gap_2() + .items_center() .rounded_sm() .bg(theme.colors.accent) .text_xs() @@ -16,12 +26,26 @@ pub fn button(id: impl Into, cx: &gpui::Context) -> Butto .font_weight(FontWeight(500.)) .px_2p5() .py_0p5(), - ) + text_color: theme.colors.accent_text, + label: None, + leading: None, + trailing: None, + } } impl Button { pub fn label(mut self, s: impl Text) -> Self { - self.0 = self.0.child(s); + self.label = Some(s.into_any_element()); + self + } + + pub fn leading(mut self, s: gpui::Svg) -> Self { + self.leading = Some(s.size_3().text_color(self.text_color)); + self + } + + pub fn trailing(mut self, s: gpui::Svg) -> Self { + self.trailing = Some(s.text_color(self.text_color)); self } } @@ -30,6 +54,16 @@ impl gpui::IntoElement for Button { type Element = gpui::Stateful; fn into_element(self) -> Self::Element { - self.0 + let mut children: Vec = Vec::with_capacity(3); + if let Some(leading) = self.leading { + children.push(leading.into_any_element()); + } + if let Some(label) = self.label { + children.push(label); + } + if let Some(trailing) = self.trailing { + children.push(trailing.into_any_element()); + } + self.div.children(children) } } diff --git a/src/component/font_icon.rs b/src/component/font_icon.rs index 2e0ea1d..071c849 100644 --- a/src/component/font_icon.rs +++ b/src/component/font_icon.rs @@ -26,9 +26,10 @@ macro_rules! define_font_icons { }; } -define_font_icons!(ChevronDown, FolderGit); +define_font_icons!(ChevronDown, FolderGit, Github); pub fn font_icon(icon: FontIcon, cx: &gpui::Context) -> gpui::Svg { let theme = cx.global::().current_theme; + println!("{}", icon_path(icon)); svg().path(icon_path(icon)).text_color(theme.colors.text) } diff --git a/src/main.rs b/src/main.rs index 259d72a..579ffda 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod colors; mod component; mod dashboard; mod query; +mod screen; mod theme; mod titlebar; @@ -22,6 +23,9 @@ fn setup_application(cx: &mut gpui::App) { api::QueryContext { http: reqwest::Client::new(), auth: None, + github: api::GithubCredentials { + client_id: "Iv23liZD4bMQpGJICsR7", + }, }, cx, ); diff --git a/src/query.rs b/src/query.rs index bae2ddf..d989094 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,16 +1,27 @@ use gpui::{AppContext, BorrowAppContext}; -use std::any::Any; +use std::{any::Any, collections::HashMap, marker::PhantomData}; -pub trait QueryFn: Clone + 'static -where - C: Context, -{ +pub trait QueryFn: Clone + 'static { type Data: 'static; type Error: 'static; + type Context: Context; fn key(&self) -> &'static str; - async fn run(&self, c: &C) -> Result; + async fn run(&self, c: &Self::Context) -> Result; +} + +struct Query { + key: &'static str, + data: QueryData, +} + +enum QueryData { + Pending, + Loading, + Stale, + Some(Box), + Err(Box), } pub enum QueryStatus<'a, Data, Error> { @@ -19,46 +30,77 @@ pub enum QueryStatus<'a, Data, Error> { Err(&'a Error), } -pub fn use_query(query_fn: F, cx: &mut gpui::Context) +#[derive(Clone)] +pub(crate) struct Entity where - C: Context + 'static, - F: QueryFn, - T: 'static, - Store: gpui::Global, + F: QueryFn, { - let ent = cx.update_global::, _>(|store, cx| store.ensure_query_data(&query_fn, cx)); + raw: gpui::Entity, + _marker: PhantomData F>, +} - cx.observe(&ent, |_, _, cx| { +pub fn use_query(query_fn: F, cx: &mut gpui::Context) -> Entity +where + F: QueryFn, + T: 'static, + Store: gpui::Global, +{ + let ent = cx + .update_global::, _>(|store, cx| store.ensure_query_data(&query_fn, cx)); + + cx.observe(&ent.raw, |_, _, cx| { cx.notify(); }) .detach(); - let query_context = cx.global::>().query_context.clone(); - + let query_context = cx.global::>().query_context.clone(); cx.observe(&query_context, move |_, _, cx| { - cx.update_global::, _>(|store, cx| { + cx.update_global::, _>(|store, cx| { store.invalidate_query(&query_fn, cx); store.ensure_query_data(&query_fn, cx); }) }) .detach(); + + ent } -pub fn read_query<'a, F, T, C>( - query_fn: F, +pub fn use_lazy_query(query_fn: F, cx: &mut gpui::Context) -> Entity +where + F: QueryFn, + T: 'static, + Store: gpui::Global, +{ + let ent = cx.update_global::, _>(|store, cx| store.entity_for(&query_fn, cx)); + + cx.observe(&ent.raw, |_, ent, cx| { + cx.notify(); + }) + .detach(); + + 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); + }) + }) + .detach(); + + ent +} + +pub fn read_query<'a, F, T>( + query: &Entity, cx: &'a gpui::Context, ) -> QueryStatus<'a, F::Data, F::Error> where - C: Context + 'static, - F: QueryFn, + F: QueryFn, T: 'static, { - let store = cx.global::>(); - let Some(ent) = store.query_data.get(query_fn.key()) else { - return QueryStatus::Loading; - }; - let QueryState { data } = ent.read(cx); - match data { + let state = query.raw.read(cx); + + match &state.data { QueryData::Loading | QueryData::Pending | QueryData::Stale => QueryStatus::Loading, QueryData::Some(data) => QueryStatus::Loaded(data.downcast_ref::().unwrap()), QueryData::Err(error) => QueryStatus::Err(error.downcast_ref::().unwrap()), @@ -67,27 +109,13 @@ where // ================= Store ================== -pub(crate) struct QueryState { - data: QueryData, -} - -pub(crate) enum QueryData { - Pending, - Loading, - Stale, - Some(Box), - Err(Box), -} - -pub(crate) type Entity = gpui::Entity; - pub(crate) trait Context: Clone {} pub struct Store where C: Context, { - query_data: std::collections::HashMap, + query_data: HashMap>, query_context: gpui::Entity, } @@ -102,29 +130,36 @@ where } } - fn entity_for(&mut self, query: &Q, cx: &mut gpui::Context) -> Entity + fn entity_for(&mut self, query: &Q, cx: &mut gpui::Context) -> Entity where - Q: QueryFn, + Q: QueryFn, T: 'static, { - self.query_data + let raw = self + .query_data .entry(query.key().into()) .or_insert_with(|| { - cx.new(|_| QueryState { + cx.new(|_| Query { + key: query.key(), data: QueryData::Pending, }) }) - .clone() + .clone(); + + Entity { + raw, + _marker: PhantomData, + } } - fn ensure_query_data(&mut self, query: &Q, cx: &mut gpui::Context) -> Entity + fn ensure_query_data(&mut self, query: &Q, cx: &mut gpui::Context) -> Entity where T: 'static, - Q: QueryFn, + Q: QueryFn, { let entity = self.entity_for(query, cx); - let should_execute = entity.read_with(cx, |state, _| { + let should_execute = entity.raw.read_with(cx, |state, _| { matches!(state.data, QueryData::Pending | QueryData::Stale) }); @@ -141,12 +176,12 @@ where cx: &mut gpui::Context, ) -> gpui::Task> where - Q: QueryFn, + Q: QueryFn, T: 'static, { let entity = self.entity_for(query, cx); - entity.update(cx, |state, cx| { + entity.raw.update(cx, |state, cx| { state.data = QueryData::Loading; cx.notify(); }); @@ -158,7 +193,7 @@ where let c = query_context.read_with(cx, |c, _| c.clone())?; let result = q.run(&c).await; - entity.update(cx, |state, cx| { + entity.raw.update(cx, |state, cx| { state.data = match result { Ok(data) => QueryData::Some(Box::new(data)), Err(err) => QueryData::Err(Box::new(err)), @@ -172,7 +207,8 @@ where fn invalidate_query(&mut self, query: &Q, cx: &mut gpui::Context) where - Q: QueryFn, + Q: QueryFn, + T: 'static, { if let Some(entity) = self.query_data.get(query.key()) { entity.update(cx, |query, cx| { @@ -183,25 +219,6 @@ where }) } } - - pub(crate) fn read_query<'a, Q, E>( - &self, - query_fn: Q, - cx: &'a gpui::Context, - ) -> QueryStatus<'a, Q::Data, Q::Error> - where - Q: QueryFn, - { - let Some(ent) = self.query_data.get(query_fn.key()) else { - return QueryStatus::Loading; - }; - let QueryState { data } = ent.read(cx); - match data { - QueryData::Loading | QueryData::Pending | QueryData::Stale => QueryStatus::Loading, - QueryData::Some(data) => QueryStatus::Loaded(data.downcast_ref::().unwrap()), - QueryData::Err(error) => QueryStatus::Err(error.downcast_ref::().unwrap()), - } - } } impl gpui::Global for Store where C: Context + 'static {} diff --git a/src/screen.rs b/src/screen.rs new file mode 100644 index 0000000..58219c9 --- /dev/null +++ b/src/screen.rs @@ -0,0 +1 @@ +pub(crate) mod welcome; diff --git a/src/screen/welcome.rs b/src/screen/welcome.rs new file mode 100644 index 0000000..c315511 --- /dev/null +++ b/src/screen/welcome.rs @@ -0,0 +1,11 @@ +struct Welcome {} + +impl gpui::Render for Welcome { + fn render( + &mut self, + window: &mut gpui::Window, + cx: &mut gpui::Context, + ) -> impl gpui::IntoElement { + todo!() + } +} diff --git a/src/titlebar.rs b/src/titlebar.rs index 361c017..a68ab55 100644 --- a/src/titlebar.rs +++ b/src/titlebar.rs @@ -2,7 +2,7 @@ use gpui::prelude::FluentBuilder; use gpui::{ParentElement, Styled, div}; use crate::component::button::button; -use crate::query::{QueryStatus, read_query, use_query}; +use crate::query::{self, QueryStatus, read_query, use_query}; use crate::{ api, app, component::{ @@ -11,14 +11,17 @@ use crate::{ }, }; -pub struct TitleBar {} +pub struct TitleBar { + fetch_user_query: query::Entity, +} pub struct RepoSelector {} impl TitleBar { pub fn new(cx: &mut gpui::Context) -> Self { - use_query(api::user::Fetch, cx); - Self {} + Self { + fetch_user_query: use_query(api::user::Fetch, cx), + } } } @@ -29,13 +32,14 @@ impl gpui::Render for TitleBar { cx: &mut gpui::Context, ) -> impl gpui::IntoElement { let g = cx.global::(); - let user = read_query(api::user::Fetch, cx); + let user = read_query(&self.fetch_user_query, cx); let user_avatar = match user { - QueryStatus::Err(api::Error::Unauthenticated) => div() - .absolute() - .right_2p5() - .child(button("login-btn", cx).label("Login")), + QueryStatus::Err(api::Error::Unauthenticated) => div().absolute().right_2p5().child( + button("login-btn", cx) + .leading(font_icon(FontIcon::Github, cx)) + .label("Login"), + ), _ => div(), };