wip: connect to github
This commit is contained in:
@@ -5,10 +5,12 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
futures = "0.3.32"
|
||||||
|
futures-lite = "2.6.1"
|
||||||
gpui = { version = "*" }
|
gpui = { version = "*" }
|
||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
rand = "0.10.1"
|
rand = "0.10.1"
|
||||||
reqwest = "0.13.2"
|
reqwest = { version = "0.13.2", features = ["form"] }
|
||||||
serde = "1.0.228"
|
serde = "1.0.228"
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
tokio = { version = "1.52.1", features = ["rt-multi-thread", "net", "time"] }
|
tokio = { version = "1.52.1", features = ["rt-multi-thread", "net", "time"] }
|
||||||
|
|||||||
60
src/api.rs
60
src/api.rs
@@ -1,3 +1,8 @@
|
|||||||
|
use std::fmt::format;
|
||||||
|
|
||||||
|
use reqwest::{Response, dns::Resolving};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::query;
|
use crate::query;
|
||||||
|
|
||||||
pub(crate) mod auth;
|
pub(crate) mod auth;
|
||||||
@@ -7,27 +12,56 @@ pub(crate) mod user;
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct QueryContext {
|
pub struct QueryContext {
|
||||||
pub(crate) http: reqwest::Client,
|
pub(crate) http: reqwest::Client,
|
||||||
pub(crate) auth: Option<Auth>,
|
pub(crate) auth: Option<AuthTokens>,
|
||||||
pub(crate) github: GithubCredentials,
|
pub(crate) github: GithubCredentials,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct Auth {
|
pub(crate) struct AuthTokens {
|
||||||
pub(crate) access_token: &'static str,
|
pub(crate) access_token: String,
|
||||||
pub(crate) refresh_token: &'static str,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct GithubCredentials {
|
pub(crate) struct GithubCredentials {
|
||||||
|
pub(crate) base_url: &'static str,
|
||||||
pub(crate) client_id: &'static str,
|
pub(crate) client_id: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Error {
|
#[derive(Debug)]
|
||||||
|
pub(crate) enum Error {
|
||||||
Unauthenticated,
|
Unauthenticated,
|
||||||
|
Github(GithubError),
|
||||||
MalformedResponse(serde_json::Error),
|
MalformedResponse(serde_json::Error),
|
||||||
HttpError(reqwest::Error),
|
HttpError(reqwest::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct GithubError {
|
||||||
|
pub error: String,
|
||||||
|
pub error_description: Option<String>,
|
||||||
|
pub error_uri: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryContext {
|
||||||
|
fn auth(&self) -> Result<&AuthTokens, Error> {
|
||||||
|
self.auth.as_ref().ok_or(Error::Unauthenticated)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn github_request(
|
||||||
|
&self,
|
||||||
|
method: reqwest::Method,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<reqwest::RequestBuilder, Error> {
|
||||||
|
let auth = self.auth()?;
|
||||||
|
Ok(self
|
||||||
|
.http
|
||||||
|
.request(method, format!("{}{}", self.github.base_url, url))
|
||||||
|
.header("Accept", "application/vnd.github+json")
|
||||||
|
.header("X-GitHub-Api-Version", "2026-03-10")
|
||||||
|
.bearer_auth(&auth.access_token))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl query::Context for QueryContext {}
|
impl query::Context for QueryContext {}
|
||||||
|
|
||||||
impl From<reqwest::Error> for Error {
|
impl From<reqwest::Error> for Error {
|
||||||
@@ -41,3 +75,19 @@ impl From<serde_json::Error> for Error {
|
|||||||
Self::MalformedResponse(value)
|
Self::MalformedResponse(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn parse_response<T>(res: reqwest::Response) -> Result<T, Error>
|
||||||
|
where
|
||||||
|
T: serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
let status = res.status().clone();
|
||||||
|
let data = res.bytes().await?;
|
||||||
|
|
||||||
|
if status.is_success() {
|
||||||
|
serde_json::from_slice::<T>(&data).map_err(|e| e.into())
|
||||||
|
} else {
|
||||||
|
serde_json::from_slice::<GithubError>(&data)
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
.and_then(|e| Err(Error::Github(e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{api, query};
|
use crate::{
|
||||||
|
api,
|
||||||
|
query::{self, use_query},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) const DEVICE_LOGIN_FLOW_URL: &str = "https://github.com/login/device";
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CreateDeviceCode;
|
pub struct CreateDeviceCode;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct DeviceCodeResponse {
|
pub(crate) struct DeviceCodeResponse {
|
||||||
device_code: String,
|
pub device_code: String,
|
||||||
user_code: String,
|
pub user_code: String,
|
||||||
vertification_uri: String,
|
pub vertification_uri: Option<String>,
|
||||||
expires_in: u16,
|
pub expires_in: u16,
|
||||||
interval: u16,
|
pub interval: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl query::QueryFn for CreateDeviceCode {
|
impl query::QueryFn for CreateDeviceCode {
|
||||||
@@ -24,17 +31,57 @@ impl query::QueryFn for CreateDeviceCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
||||||
let data = c
|
let res = c
|
||||||
.http
|
.http
|
||||||
.post(format!(
|
.post(format!(
|
||||||
"https://github.com/login/device/code?client_id={}",
|
"https://github.com/login/device/code?client_id={}",
|
||||||
c.github.client_id
|
c.github.client_id
|
||||||
))
|
))
|
||||||
|
.header("Accept", "application/json")
|
||||||
.send()
|
.send()
|
||||||
.await?
|
|
||||||
.bytes()
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
serde_json::from_slice::<DeviceCodeResponse>(&data).map_err(|e| e.into())
|
api::parse_response(res).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RequestAccessToken {
|
||||||
|
pub device_code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct RequestAccessTokenResponse {
|
||||||
|
pub access_token: String,
|
||||||
|
pub token_type: String,
|
||||||
|
pub scope: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl query::QueryFn for RequestAccessToken {
|
||||||
|
type Data = RequestAccessTokenResponse;
|
||||||
|
type Error = api::Error;
|
||||||
|
type Context = api::QueryContext;
|
||||||
|
|
||||||
|
fn key(&self) -> &'static str {
|
||||||
|
"auth.access_token"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
||||||
|
let mut params = HashMap::new();
|
||||||
|
params.insert("client_id", c.github.client_id);
|
||||||
|
params.insert("device_code", &self.device_code);
|
||||||
|
params.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
|
||||||
|
|
||||||
|
let res = c
|
||||||
|
.http
|
||||||
|
.post(format!(
|
||||||
|
"https://github.com/login/oauth/access_token?client_id={}&device_code={}",
|
||||||
|
c.github.client_id, self.device_code
|
||||||
|
))
|
||||||
|
.form(¶ms)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
api::parse_response(res).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
|
use reqwest::Method;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{api, query};
|
use crate::{api, query};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
pub login: String,
|
||||||
|
pub id: u64,
|
||||||
|
pub avatar_url: String,
|
||||||
|
pub html_url: String,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Fetch;
|
pub struct Fetch;
|
||||||
|
|
||||||
impl query::QueryFn for Fetch {
|
impl query::QueryFn for Fetch {
|
||||||
type Data = api::Error;
|
type Data = User;
|
||||||
type Error = api::Error;
|
type Error = api::Error;
|
||||||
type Context = api::QueryContext;
|
type Context = api::QueryContext;
|
||||||
|
|
||||||
@@ -13,6 +26,7 @@ impl query::QueryFn for Fetch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
||||||
Err(api::Error::Unauthenticated)
|
let res = c.github_request(Method::GET, "/user")?.send().await?;
|
||||||
|
api::parse_response(res).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,6 @@ pub fn rng(cx: &mut gpui::App) -> &mut rand::prelude::ThreadRng {
|
|||||||
&mut cx.global_mut::<Global>().rng
|
&mut cx.global_mut::<Global>().rng
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn query_store<'a, E>(cx: &'a gpui::Context<E>) -> &'a query::Store<api::QueryContext> {
|
pub fn query_store(cx: &gpui::App) -> &query::Store<api::QueryContext> {
|
||||||
cx.global::<query::Store<api::QueryContext>>()
|
cx.global::<query::Store<api::QueryContext>>()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use gpui::{
|
|||||||
StatefulInteractiveElement, Styled, div, prelude::FluentBuilder,
|
StatefulInteractiveElement, Styled, div, prelude::FluentBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{app, component::text::TextContent, theme};
|
use crate::{app, component::text::TextContent};
|
||||||
|
|
||||||
#[derive(gpui::IntoElement)]
|
#[derive(gpui::IntoElement)]
|
||||||
pub struct Button {
|
pub struct Button {
|
||||||
@@ -12,6 +12,7 @@ pub struct Button {
|
|||||||
leading: Option<gpui::Svg>,
|
leading: Option<gpui::Svg>,
|
||||||
trailing: Option<gpui::Svg>,
|
trailing: Option<gpui::Svg>,
|
||||||
on_click: Option<Box<dyn Fn(&gpui::ClickEvent, &mut gpui::Window, &mut gpui::App)>>,
|
on_click: Option<Box<dyn Fn(&gpui::ClickEvent, &mut gpui::Window, &mut gpui::App)>>,
|
||||||
|
enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn button(id: impl Into<gpui::ElementId>) -> Button {
|
pub fn button(id: impl Into<gpui::ElementId>) -> Button {
|
||||||
@@ -21,6 +22,7 @@ pub fn button(id: impl Into<gpui::ElementId>) -> Button {
|
|||||||
leading: None,
|
leading: None,
|
||||||
trailing: None,
|
trailing: None,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
|
enabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +49,11 @@ impl Button {
|
|||||||
self.on_click = Some(Box::new(f));
|
self.on_click = Some(Box::new(f));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn disabled(mut self) -> Self {
|
||||||
|
self.enabled = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl gpui::RenderOnce for Button {
|
impl gpui::RenderOnce for Button {
|
||||||
@@ -86,8 +93,9 @@ impl gpui::RenderOnce for Button {
|
|||||||
.px_2p5()
|
.px_2p5()
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.children(children)
|
.children(children)
|
||||||
.when(self.on_click.is_some(), |div| {
|
.when(self.on_click.is_some() && self.enabled, |div| {
|
||||||
div.on_click(self.on_click.unwrap())
|
div.on_click(self.on_click.unwrap())
|
||||||
})
|
})
|
||||||
|
.when(!self.enabled, |div| div.opacity(0.5))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/main.rs
10
src/main.rs
@@ -10,6 +10,7 @@ mod component;
|
|||||||
mod dashboard;
|
mod dashboard;
|
||||||
mod query;
|
mod query;
|
||||||
mod screen;
|
mod screen;
|
||||||
|
mod storage;
|
||||||
mod theme;
|
mod theme;
|
||||||
mod titlebar;
|
mod titlebar;
|
||||||
|
|
||||||
@@ -29,16 +30,15 @@ fn main() {
|
|||||||
|
|
||||||
fn setup_application(cx: &mut gpui::App) {
|
fn setup_application(cx: &mut gpui::App) {
|
||||||
let window_bounds = gpui::Bounds::centered(None, size(px(800.), px(600.0)), cx);
|
let window_bounds = gpui::Bounds::centered(None, size(px(800.), px(600.0)), cx);
|
||||||
let query_store = query::Store::new(
|
|
||||||
api::QueryContext {
|
let query_store = query::Store::new(api::QueryContext {
|
||||||
http: reqwest::Client::new(),
|
http: reqwest::Client::new(),
|
||||||
auth: None,
|
auth: None,
|
||||||
github: api::GithubCredentials {
|
github: api::GithubCredentials {
|
||||||
|
base_url: "https://api.github.com",
|
||||||
client_id: "Iv23liZD4bMQpGJICsR7",
|
client_id: "Iv23liZD4bMQpGJICsR7",
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
let global = app::Global {
|
let global = app::Global {
|
||||||
safe_area: bounds(point(px(0.), px(0.)), size(px(72.), px(12.))),
|
safe_area: bounds(point(px(0.), px(0.)), size(px(72.), px(12.))),
|
||||||
|
|||||||
233
src/query.rs
233
src/query.rs
@@ -1,9 +1,9 @@
|
|||||||
use gpui::{AppContext, BorrowAppContext};
|
use gpui::{AppContext, BorrowAppContext};
|
||||||
use std::{any::Any, collections::HashMap, marker::PhantomData};
|
use std::{any::Any, collections::HashMap, marker::PhantomData, ops::Deref};
|
||||||
|
|
||||||
pub trait QueryFn: Clone + 'static {
|
pub(crate) trait QueryFn: Clone + 'static {
|
||||||
type Data: 'static;
|
type Data: 'static;
|
||||||
type Error: 'static;
|
type Error: std::fmt::Debug + 'static;
|
||||||
type Context: Context;
|
type Context: Context;
|
||||||
|
|
||||||
fn key(&self) -> &'static str;
|
fn key(&self) -> &'static str;
|
||||||
@@ -11,7 +11,13 @@ pub trait QueryFn: Clone + 'static {
|
|||||||
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error>;
|
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Query {
|
pub(crate) trait QueryAppContext: gpui::AppContext {
|
||||||
|
fn ready<T>(value: T) -> Self::Result<T>;
|
||||||
|
|
||||||
|
fn map_result<T, U>(result: Self::Result<T>, f: impl FnOnce(T) -> U) -> Self::Result<U>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Query {
|
||||||
key: &'static str,
|
key: &'static str,
|
||||||
data: QueryData,
|
data: QueryData,
|
||||||
}
|
}
|
||||||
@@ -51,7 +57,7 @@ where
|
|||||||
ent
|
ent
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.observe(&ent.raw, move |_, ent, cx| {
|
cx.observe(&ent, move |_, ent, cx| {
|
||||||
let query = ent.read(cx);
|
let query = ent.read(cx);
|
||||||
if matches!(query.data, QueryData::Stale) {
|
if matches!(query.data, QueryData::Stale) {
|
||||||
cx.update_global::<Store<F::Context>, _>(|store, cx| {
|
cx.update_global::<Store<F::Context>, _>(|store, cx| {
|
||||||
@@ -62,15 +68,6 @@ where
|
|||||||
})
|
})
|
||||||
.detach();
|
.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(&cloned_ent, cx);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
ent
|
ent
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +79,7 @@ where
|
|||||||
{
|
{
|
||||||
let ent = cx.update_global::<Store<F::Context>, _>(|store, cx| store.entity_for(&query_fn, cx));
|
let ent = cx.update_global::<Store<F::Context>, _>(|store, cx| store.entity_for(&query_fn, cx));
|
||||||
|
|
||||||
cx.observe(&ent.raw, move |_, ent, cx| {
|
cx.observe(&ent, move |_, ent, cx| {
|
||||||
let query = ent.read(cx);
|
let query = ent.read(cx);
|
||||||
if matches!(query.data, QueryData::Stale) {
|
if matches!(query.data, QueryData::Stale) {
|
||||||
cx.update_global::<Store<F::Context>, _>(|store, cx| {
|
cx.update_global::<Store<F::Context>, _>(|store, cx| {
|
||||||
@@ -93,37 +90,91 @@ where
|
|||||||
})
|
})
|
||||||
.detach();
|
.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(&cloned_ent, cx);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
ent
|
ent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_query<F>(query_fn: F, cx: &mut gpui::AsyncApp) -> anyhow::Result<Entity<F>>
|
||||||
|
where
|
||||||
|
F: QueryFn,
|
||||||
|
{
|
||||||
|
let ent = cx.update(|cx| {
|
||||||
|
cx.update_global::<Store<F::Context>, _>(|store, cx| store.ensure_query_data(&query_fn, cx))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
enum WaitState {
|
||||||
|
Cached,
|
||||||
|
Waiting {
|
||||||
|
rx: futures::channel::oneshot::Receiver<()>,
|
||||||
|
sub: gpui::Subscription,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let wait_state = cx.update(|cx| {
|
||||||
|
let is_done = ent.read_with(cx, |query, _| {
|
||||||
|
matches!(query.data, QueryData::Some(_) | QueryData::Err(_))
|
||||||
|
});
|
||||||
|
if is_done {
|
||||||
|
WaitState::Cached
|
||||||
|
} else {
|
||||||
|
let (tx, rx) = futures::channel::oneshot::channel();
|
||||||
|
let ent = ent.clone();
|
||||||
|
let mut tx = Some(tx);
|
||||||
|
|
||||||
|
let sub = cx.observe(&ent, move |ent, cx| {
|
||||||
|
let is_done = ent.read_with(cx, |query, _| {
|
||||||
|
matches!(query.data, QueryData::Some(_) | QueryData::Err(_))
|
||||||
|
});
|
||||||
|
if is_done && let Some(tx) = tx.take() {
|
||||||
|
tx.send(());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
WaitState::Waiting { rx, sub }
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match wait_state {
|
||||||
|
WaitState::Cached => {
|
||||||
|
return Ok(ent);
|
||||||
|
}
|
||||||
|
WaitState::Waiting { rx, sub } => {
|
||||||
|
let _sub = sub;
|
||||||
|
let _ = rx.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<F> Entity<F>
|
impl<F> Entity<F>
|
||||||
where
|
where
|
||||||
F: QueryFn,
|
F: QueryFn,
|
||||||
Store<F::Context>: gpui::Global,
|
Store<F::Context>: gpui::Global,
|
||||||
{
|
{
|
||||||
pub fn refetch(&self, cx: &mut gpui::Context<F::Context>) {
|
pub fn refetch<E>(&self, cx: &mut gpui::Context<E>)
|
||||||
|
where
|
||||||
|
E: 'static,
|
||||||
|
{
|
||||||
cx.update_global::<Store<F::Context>, _>(|store, cx| {
|
cx.update_global::<Store<F::Context>, _>(|store, cx| {
|
||||||
store.invalidate_query(self, cx);
|
store.invalidate_query(self, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_query<'a, F, T>(
|
impl<F> Deref for Entity<F>
|
||||||
query: &Entity<F>,
|
where
|
||||||
cx: &'a gpui::Context<T>,
|
F: QueryFn,
|
||||||
) -> QueryStatus<'a, F::Data, F::Error>
|
{
|
||||||
|
type Target = gpui::Entity<Query>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_query<'a, F>(query: &Entity<F>, cx: &'a gpui::App) -> QueryStatus<'a, F::Data, F::Error>
|
||||||
where
|
where
|
||||||
F: QueryFn,
|
F: QueryFn,
|
||||||
T: 'static,
|
|
||||||
{
|
{
|
||||||
let state = query.raw.read(cx);
|
let state = query.raw.read(cx);
|
||||||
|
|
||||||
@@ -143,45 +194,55 @@ where
|
|||||||
C: Context,
|
C: Context,
|
||||||
{
|
{
|
||||||
query_data: HashMap<String, gpui::Entity<Query>>,
|
query_data: HashMap<String, gpui::Entity<Query>>,
|
||||||
query_context: gpui::Entity<C>,
|
query_context: C,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C> Store<C>
|
impl<C> Store<C>
|
||||||
where
|
where
|
||||||
C: Context + 'static,
|
C: Context + 'static,
|
||||||
{
|
{
|
||||||
pub fn new(ctx: C, cx: &mut gpui::App) -> Self {
|
pub fn new(ctx: C) -> Self {
|
||||||
Self {
|
Self {
|
||||||
query_context: cx.new(|_| ctx),
|
query_context: ctx,
|
||||||
query_data: std::collections::HashMap::new(),
|
query_data: std::collections::HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entity_for<Q, T>(&mut self, query: &Q, cx: &mut gpui::Context<T>) -> Entity<Q>
|
pub(crate) fn update_query_context(&mut self, f: impl FnOnce(&mut C)) {
|
||||||
|
f(&mut self.query_context);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_for<Q, CX>(&mut self, query: &Q, cx: &mut CX) -> CX::Result<Entity<Q>>
|
||||||
where
|
where
|
||||||
Q: QueryFn<Context = C>,
|
Q: QueryFn<Context = C>,
|
||||||
T: 'static,
|
CX: QueryAppContext,
|
||||||
{
|
{
|
||||||
let raw = self
|
if let Some(raw) = self.query_data.get(query.key()) {
|
||||||
.query_data
|
return CX::ready(Entity {
|
||||||
.entry(query.key().into())
|
raw: raw.clone(),
|
||||||
.or_insert_with(|| {
|
_marker: PhantomData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = query.key();
|
||||||
|
|
||||||
|
CX::map_result(
|
||||||
cx.new(|_| Query {
|
cx.new(|_| Query {
|
||||||
key: query.key(),
|
key: query.key(),
|
||||||
data: QueryData::Pending,
|
data: QueryData::Pending,
|
||||||
})
|
}),
|
||||||
})
|
|raw| {
|
||||||
.clone();
|
self.query_data.insert(key.into(), raw.clone());
|
||||||
|
|
||||||
Entity {
|
Entity {
|
||||||
raw,
|
raw,
|
||||||
_marker: PhantomData,
|
_marker: PhantomData,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_query_data<Q, T>(&mut self, query: &Q, cx: &mut gpui::Context<T>) -> Entity<Q>
|
fn ensure_query_data<Q>(&mut self, query: &Q, cx: &mut gpui::App) -> Entity<Q>
|
||||||
where
|
where
|
||||||
T: 'static,
|
|
||||||
Q: QueryFn<Context = C>,
|
Q: QueryFn<Context = C>,
|
||||||
{
|
{
|
||||||
let entity = self.entity_for(query, cx);
|
let entity = self.entity_for(query, cx);
|
||||||
@@ -191,20 +252,19 @@ where
|
|||||||
});
|
});
|
||||||
|
|
||||||
if should_execute {
|
if should_execute {
|
||||||
self.execute_query(query, cx).detach();
|
self.execute_query_detached(query, cx).detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
entity
|
entity
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_query<Q, T>(
|
fn execute_query_detached<Q>(
|
||||||
&mut self,
|
&mut self,
|
||||||
query: &Q,
|
query: &Q,
|
||||||
cx: &mut gpui::Context<T>,
|
cx: &mut gpui::App,
|
||||||
) -> gpui::Task<anyhow::Result<()>>
|
) -> gpui::Task<anyhow::Result<()>>
|
||||||
where
|
where
|
||||||
Q: QueryFn<Context = C>,
|
Q: QueryFn<Context = C>,
|
||||||
T: 'static,
|
|
||||||
{
|
{
|
||||||
let entity = self.entity_for(query, cx);
|
let entity = self.entity_for(query, cx);
|
||||||
|
|
||||||
@@ -216,14 +276,21 @@ where
|
|||||||
let q = query.clone();
|
let q = query.clone();
|
||||||
let query_context = self.query_context.clone();
|
let query_context = self.query_context.clone();
|
||||||
|
|
||||||
cx.spawn(async move |_, cx| {
|
cx.spawn(async move |cx| {
|
||||||
let c = query_context.read_with(cx, |c, _| c.clone())?;
|
println!("[query] {}", q.key());
|
||||||
let result = q.run(&c).await;
|
|
||||||
|
let result = q.run(&query_context).await;
|
||||||
|
|
||||||
entity.raw.update(cx, |state, cx| {
|
entity.raw.update(cx, |state, cx| {
|
||||||
state.data = match result {
|
state.data = match result {
|
||||||
Ok(data) => QueryData::Some(Box::new(data)),
|
Ok(data) => {
|
||||||
Err(err) => QueryData::Err(Box::new(err)),
|
println!("[query] OK {}", q.key());
|
||||||
|
QueryData::Some(Box::new(data))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("[query] ERR {:?}: {:?}", q.key(), err);
|
||||||
|
QueryData::Err(Box::new(err))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
@@ -232,6 +299,34 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn execute_query<'a, F>(&mut self, query_fn: &F, cx: &'a mut gpui::AsyncApp) -> Entity<F>
|
||||||
|
where
|
||||||
|
F: QueryFn<Context = C>,
|
||||||
|
{
|
||||||
|
let entity = self.entity_for(query_fn, cx).unwrap();
|
||||||
|
|
||||||
|
entity.update(cx, |query, cx| {
|
||||||
|
query.data = QueryData::Loading;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = query_fn.run(&self.query_context).await;
|
||||||
|
|
||||||
|
entity
|
||||||
|
.raw
|
||||||
|
.update(cx, |query, cx| {
|
||||||
|
query.data = match result {
|
||||||
|
Ok(data) => QueryData::Some(Box::new(data)),
|
||||||
|
Err(err) => QueryData::Err(Box::new(err)),
|
||||||
|
};
|
||||||
|
cx.notify();
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
entity
|
||||||
|
}
|
||||||
|
|
||||||
fn invalidate_query<E, F>(&self, entity: &Entity<F>, cx: &mut gpui::Context<E>)
|
fn invalidate_query<E, F>(&self, entity: &Entity<F>, cx: &mut gpui::Context<E>)
|
||||||
where
|
where
|
||||||
E: 'static,
|
E: 'static,
|
||||||
@@ -249,4 +344,34 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl QueryAppContext for gpui::App {
|
||||||
|
fn ready<T>(value: T) -> Self::Result<T> {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_result<T, U>(result: Self::Result<T>, f: impl FnOnce(T) -> U) -> Self::Result<U> {
|
||||||
|
f(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryAppContext for gpui::AsyncApp {
|
||||||
|
fn ready<T>(value: T) -> Self::Result<T> {
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_result<T, U>(result: Self::Result<T>, f: impl FnOnce(T) -> U) -> Self::Result<U> {
|
||||||
|
result.map(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, E> QueryAppContext for gpui::Context<'a, E> {
|
||||||
|
fn ready<T>(value: T) -> Self::Result<T> {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_result<T, U>(result: Self::Result<T>, f: impl FnOnce(T) -> U) -> Self::Result<U> {
|
||||||
|
f(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<C> gpui::Global for Store<C> where C: Context + 'static {}
|
impl<C> gpui::Global for Store<C> where C: Context + 'static {}
|
||||||
|
|||||||
@@ -1,31 +1,95 @@
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::Duration;
|
||||||
|
|
||||||
use gpui::{AppContext, FontWeight, ParentElement, Styled, div, prelude::FluentBuilder};
|
use futures_lite::StreamExt;
|
||||||
|
use gpui::{
|
||||||
|
BorrowAppContext, InteractiveElement, ParentElement, StatefulInteractiveElement, Styled, Timer,
|
||||||
|
div, prelude::FluentBuilder,
|
||||||
|
};
|
||||||
use rand::RngExt;
|
use rand::RngExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api, app,
|
api, app,
|
||||||
component::text::text,
|
component::{button::button, text::text},
|
||||||
query::{self, QueryStatus, read_query, use_lazy_query},
|
query::{self, QueryStatus, fetch_query, read_query, use_query},
|
||||||
|
storage, theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) struct GithubStepView {
|
pub(crate) struct GithubStepView {
|
||||||
last_tick: Instant,
|
has_opened_link: bool,
|
||||||
placeholder_code: String,
|
placeholder_code: String,
|
||||||
|
|
||||||
create_device_code_query: query::Entity<api::auth::CreateDeviceCode>,
|
create_device_code_query: query::Entity<api::auth::CreateDeviceCode>,
|
||||||
|
request_access_token_query: Option<query::Entity<api::auth::RequestAccessToken>>,
|
||||||
|
user_query: Option<query::Entity<api::user::Fetch>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new(cx: &mut gpui::Context<GithubStepView>) -> GithubStepView {
|
pub(crate) fn new(cx: &mut gpui::Context<GithubStepView>) -> GithubStepView {
|
||||||
GithubStepView {
|
let mut view = GithubStepView {
|
||||||
last_tick: Instant::now(),
|
has_opened_link: false,
|
||||||
placeholder_code: "ABCDEFGH".to_owned(),
|
placeholder_code: "ABCDEFGH".to_owned(),
|
||||||
create_device_code_query: use_lazy_query(api::auth::CreateDeviceCode, cx),
|
|
||||||
}
|
create_device_code_query: use_query(api::auth::CreateDeviceCode, cx),
|
||||||
|
request_access_token_query: None,
|
||||||
|
user_query: None,
|
||||||
|
};
|
||||||
|
view.on_create(cx);
|
||||||
|
view
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GithubStepView {
|
impl GithubStepView {
|
||||||
const CHAR_POOL: &'static str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
const CHAR_POOL: &'static str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
|
||||||
|
fn on_create(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
|
cx.observe(&self.create_device_code_query, |this, _, cx| {
|
||||||
|
let code = {
|
||||||
|
let data = read_query(&this.create_device_code_query, cx);
|
||||||
|
if let QueryStatus::Loaded(data) = data {
|
||||||
|
Some(data.device_code.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(ref code) = code
|
||||||
|
&& !this.has_opened_link
|
||||||
|
{
|
||||||
|
this.has_opened_link = true;
|
||||||
|
this.begin_auth_flow(code, cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.spawn(async |this, cx| {
|
||||||
|
let mut timer = Timer::interval(Duration::from_millis(50));
|
||||||
|
loop {
|
||||||
|
let is_code_loaded = this.read_with(cx, |this, cx| {
|
||||||
|
matches!(
|
||||||
|
read_query(&this.create_device_code_query, cx),
|
||||||
|
QueryStatus::Loaded(_)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if matches!(is_code_loaded, Ok(true) | Err(_)) {
|
||||||
|
timer.clear();
|
||||||
|
} else {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.placeholder_code = this.generate_random_code(cx);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let None = timer.next().await {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_github_auth_page(cx: &mut gpui::Context<Self>) {
|
||||||
|
cx.open_url(api::auth::DEVICE_LOGIN_FLOW_URL);
|
||||||
|
}
|
||||||
|
|
||||||
fn generate_random_code(&mut self, cx: &mut gpui::Context<Self>) -> String {
|
fn generate_random_code(&mut self, cx: &mut gpui::Context<Self>) -> String {
|
||||||
let rng = app::rng(cx);
|
let rng = app::rng(cx);
|
||||||
(0..8)
|
(0..8)
|
||||||
@@ -35,37 +99,167 @@ impl GithubStepView {
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn copy_device_code(&self, cx: &gpui::App) {
|
||||||
|
let query = read_query(&self.create_device_code_query, cx);
|
||||||
|
match query {
|
||||||
|
QueryStatus::Loaded(data) => {
|
||||||
|
cx.write_to_clipboard(gpui::ClipboardItem::new_string(data.user_code.clone()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_auth_flow(&mut self, device_code: &str, cx: &mut gpui::Context<Self>) {
|
||||||
|
GithubStepView::open_github_auth_page(cx);
|
||||||
|
|
||||||
|
let query = use_query(
|
||||||
|
api::auth::RequestAccessToken {
|
||||||
|
device_code: device_code.to_owned(),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.observe(&query, |this, _, cx| {
|
||||||
|
this.handle_access_token_query_response(cx);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
self.has_opened_link = true;
|
||||||
|
self.request_access_token_query = Some(query);
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_access_token_query_response(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
|
let Some(query) = &self.request_access_token_query else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match read_query(query, cx) {
|
||||||
|
QueryStatus::Loaded(data) => {
|
||||||
|
let auth_tokens = api::AuthTokens {
|
||||||
|
access_token: data.access_token.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.update_global::<query::Store<api::QueryContext>, _>(|store, _| {
|
||||||
|
store.update_query_context(|c| {
|
||||||
|
c.auth = Some(auth_tokens.clone());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn(async move |weak, cx| {
|
||||||
|
let ent = fetch_query(api::user::Fetch, cx).await;
|
||||||
|
|
||||||
|
let fut = weak
|
||||||
|
.update(cx, move |_this, cx| {
|
||||||
|
let Ok(query) = ent else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let QueryStatus::Loaded(user) = read_query(&query, cx) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(storage::store_auth_tokens(&auth_tokens, user, cx))
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let r = if let Some(task) = fut {
|
||||||
|
task.await
|
||||||
|
} else {
|
||||||
|
Err(anyhow::Error::msg(""))
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryStatus::Err(api::Error::Github(api::GithubError { error, .. })) => {
|
||||||
|
if error == "authorization_pending" {
|
||||||
|
cx.spawn(async |weak, cx| {
|
||||||
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
|
if let Ok(Some(query)) =
|
||||||
|
weak.read_with(cx, |this, _cx| this.request_access_token_query.clone())
|
||||||
|
{
|
||||||
|
weak.update(cx, |_this, cx| {
|
||||||
|
query.refetch(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl gpui::Render for GithubStepView {
|
impl gpui::Render for GithubStepView {
|
||||||
fn render(
|
fn render(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: &mut gpui::Window,
|
_window: &mut gpui::Window,
|
||||||
cx: &mut gpui::Context<Self>,
|
cx: &mut gpui::Context<Self>,
|
||||||
) -> impl gpui::IntoElement {
|
) -> impl gpui::IntoElement {
|
||||||
let theme = app::current_theme(cx);
|
|
||||||
|
|
||||||
let border_color = theme.colors.surface_elevated.clone();
|
|
||||||
let bg_color = theme.colors.surface.clone();
|
|
||||||
|
|
||||||
let create_device_code_query = read_query(&self.create_device_code_query, cx);
|
let create_device_code_query = read_query(&self.create_device_code_query, cx);
|
||||||
let is_loading_code = matches!(create_device_code_query, QueryStatus::Loading);
|
let is_loading_code = matches!(create_device_code_query, QueryStatus::Loading);
|
||||||
|
|
||||||
let now = Instant::now();
|
let theme = app::current_theme(cx);
|
||||||
let should_tick = now.duration_since(self.last_tick) >= Duration::from_millis(50);
|
|
||||||
|
|
||||||
if is_loading_code {
|
let displayed_code = match create_device_code_query {
|
||||||
cx.on_next_frame(window, move |this, _, cx| {
|
QueryStatus::Loaded(data) => &data.user_code,
|
||||||
if should_tick {
|
_ => &self.placeholder_code,
|
||||||
this.placeholder_code = this.generate_random_code(cx);
|
};
|
||||||
this.last_tick = Instant::now();
|
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.size_full()
|
||||||
|
.px_4()
|
||||||
|
.pt_12()
|
||||||
|
.child(header())
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.flex_1()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(
|
||||||
|
device_code_area(displayed_code, is_loading_code, theme).on_click(
|
||||||
|
cx.listener(|this, _, _, cx| {
|
||||||
|
this.copy_device_code(cx);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
text(if is_loading_code {
|
||||||
|
"Loading..."
|
||||||
|
} else {
|
||||||
|
"Click to copy"
|
||||||
|
})
|
||||||
|
.text_sm()
|
||||||
|
.opacity(0.5),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div().flex().flex_row().justify_end().w_full().pb_4().child(
|
||||||
|
button("connect-to-github-next")
|
||||||
|
.label("Next")
|
||||||
|
.when(is_loading_code, |it| it.disabled()),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let letter_boxes = self
|
fn device_code_area(
|
||||||
.placeholder_code
|
code: &String,
|
||||||
|
is_loading: bool,
|
||||||
|
theme: &theme::Theme,
|
||||||
|
) -> gpui::Stateful<gpui::Div> {
|
||||||
|
let border_color = theme.colors.border.clone();
|
||||||
|
let bg_color = theme.colors.background.clone();
|
||||||
|
|
||||||
|
let letter_boxes = code
|
||||||
.split("")
|
.split("")
|
||||||
.filter(|c| !c.is_empty())
|
.filter(|c| !c.is_empty())
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
@@ -80,28 +274,18 @@ impl gpui::Render for GithubStepView {
|
|||||||
.rounded_lg()
|
.rounded_lg()
|
||||||
.bg(bg_color)
|
.bg(bg_color)
|
||||||
})
|
})
|
||||||
.when(is_loading_code, |it| it.opacity(0.5))
|
.when(is_loading, |it| it.opacity(0.5))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.flex()
|
.id("github-device-code-area")
|
||||||
.flex_col()
|
|
||||||
.size_full()
|
|
||||||
.px_4()
|
|
||||||
.py_12()
|
|
||||||
.child(header())
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
.flex()
|
||||||
.flex_row()
|
.flex_row()
|
||||||
.flex_1()
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.children(letter_boxes),
|
.children(letter_boxes)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header() -> impl gpui::IntoElement {
|
fn header() -> impl gpui::IntoElement {
|
||||||
|
|||||||
@@ -59,8 +59,6 @@ impl gpui::Render for Screen {
|
|||||||
let theme = app::current_theme(cx);
|
let theme = app::current_theme(cx);
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id("awd")
|
|
||||||
.on_click(cx.listener(|a, b, c, d| {}))
|
|
||||||
.flex()
|
.flex()
|
||||||
.flex_row()
|
.flex_row()
|
||||||
.items_center()
|
.items_center()
|
||||||
|
|||||||
13
src/storage.rs
Normal file
13
src/storage.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use crate::api;
|
||||||
|
|
||||||
|
pub(crate) fn store_auth_tokens(
|
||||||
|
tokens: &api::AuthTokens,
|
||||||
|
user: &api::user::User,
|
||||||
|
cx: &gpui::App,
|
||||||
|
) -> gpui::Task<anyhow::Result<()>> {
|
||||||
|
cx.write_credentials(
|
||||||
|
&format!("novem://github/github.com/user/{}", user.id),
|
||||||
|
&format!("{}", user.id),
|
||||||
|
tokens.access_token.as_bytes(),
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user