diff --git a/src/api.rs b/src/api.rs index bad29f1..84d0053 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,7 +1,7 @@ use std::fmt::format; use reqwest::{Response, dns::Resolving}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::query; @@ -16,7 +16,7 @@ pub struct QueryContext { pub(crate) github: GithubCredentials, } -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] pub(crate) struct AuthTokens { pub(crate) access_token: String, } diff --git a/src/app.rs b/src/app.rs index 0dc6408..e3fb5c6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,10 +1,6 @@ -use gpui::{div, prelude::*}; - -use crate::dashboard; +use crate::api; use crate::query; use crate::theme; -use crate::titlebar; -use crate::{api, app}; pub struct Global { pub safe_area: gpui::Bounds, @@ -13,45 +9,6 @@ pub struct Global { pub rng: rand::prelude::ThreadRng, } -pub struct Chrome {} - -impl Chrome { - pub fn new(window: &mut gpui::Window, cx: &mut gpui::Context) -> Self { - cx.observe_window_appearance(window, |_, window, cx| { - cx.update_global::(|global, cx| { - global.current_theme = global - .theme_family - .theme_for_appearance(window.appearance()); - cx.notify(); - }); - }) - .detach(); - - Self {} - } -} - -impl gpui::Render for Chrome { - fn render( - &mut self, - _window: &mut gpui::Window, - cx: &mut gpui::Context, - ) -> impl gpui::IntoElement { - let title_bar = cx.new(|cx| titlebar::TitleBar::new(cx)); - - let dashboard = cx.new(|_| dashboard::Screen { - text: "World".into(), - }); - - div() - .flex() - .flex_col() - .size_full() - .child(title_bar) - .child(dashboard) - } -} - impl gpui::Global for Global {} pub fn current_theme(cx: &gpui::App) -> &theme::Theme { diff --git a/src/asset/font_icon/cat.svg b/src/asset/font_icon/cat.svg new file mode 100644 index 0000000..67100b3 --- /dev/null +++ b/src/asset/font_icon/cat.svg @@ -0,0 +1 @@ + diff --git a/src/component/font_icon.rs b/src/component/font_icon.rs index 16813e1..5c9b089 100644 --- a/src/component/font_icon.rs +++ b/src/component/font_icon.rs @@ -26,7 +26,7 @@ macro_rules! define_font_icons { }; } -define_font_icons!(Check, ChevronDown, FolderGit, Github, ArrowRight); +define_font_icons!(Check, ChevronDown, FolderGit, Github, ArrowRight, Cat); #[derive(gpui::IntoElement)] pub struct FontIconSvg { diff --git a/src/dashboard.rs b/src/dashboard.rs deleted file mode 100644 index aa947ae..0000000 --- a/src/dashboard.rs +++ /dev/null @@ -1,48 +0,0 @@ -use gpui::{div, prelude::*}; - -use crate::app; - -pub struct Screen { - pub text: gpui::SharedString, -} - -impl Render for Screen { - fn render( - &mut self, - _window: &mut gpui::Window, - cx: &mut gpui::Context, - ) -> impl IntoElement { - let theme = app::current_theme(cx); - - div() - .flex() - .flex_1() - .flex_row() - .w_full() - .gap_2() - .p_2p5() - .pt_0() - .bg(theme.colors.background) - .justify_center() - .items_center() - .shadow_lg() - .text_xl() - .text_color(theme.colors.text) - .child( - div() - .h_full() - .flex() - .w_1_3() - .bg(theme.colors.surface) - .rounded_lg(), - ) - .child( - div() - .h_full() - .flex() - .w_2_3() - .bg(theme.colors.surface) - .rounded_lg(), - ) - } -} diff --git a/src/main.rs b/src/main.rs index 7a07f03..28778d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,23 @@ use gpui::{bounds, point, prelude::*, px, size}; -use crate::screen::setup_wizard; +use crate::{query::fetch_query, screen::dashboard, screen::setup_wizard}; mod api; mod app; mod asset; mod colors; mod component; -mod dashboard; mod http; mod query; mod screen; mod storage; mod theme; -mod titlebar; mod util; enum Start { FromScratch, FromSetup(setup_wizard::StoredSetupState), - FromSaved, + FromSaved(storage::PersistedState), } fn main() { @@ -76,19 +74,25 @@ fn setup_application(cx: &mut gpui::App) { _ = setup_wizard::open_window(screen, cx); } - _ => {} + Start::FromSaved(_) => { + let screen = dashboard::new(cx); + _ = dashboard::open_window(screen, cx); + } }; } fn resume_application_state(cx: &mut gpui::App) -> Start { let state = storage::load_persisted_state(); - let Some(state) = state else { + let Some(mut state) = state else { return Start::FromScratch; }; - let auth_tokens = cx - .background_executor() - .block(storage::load_auth_tokens(cx, state.selected_account)); + let auth_tokens = if cfg!(debug_assertions) { + state.debug_auth_tokens.take() + } else { + cx.background_executor() + .block(storage::load_auth_tokens(cx, state.selected_account)) + }; let Some(auth_tokens) = auth_tokens else { return Start::FromScratch; }; @@ -106,6 +110,6 @@ fn resume_application_state(cx: &mut gpui::App) -> Start { match setup_status { setup_wizard::SetupStatus::NotStarted => Start::FromScratch, setup_wizard::SetupStatus::InProgress(state) => Start::FromSetup(state), - setup_wizard::SetupStatus::Completed => Start::FromSaved, + setup_wizard::SetupStatus::Completed => Start::FromSaved(state), } } diff --git a/src/screen/dashboard/mod.rs b/src/screen/dashboard/mod.rs new file mode 100644 index 0000000..b88c8ec --- /dev/null +++ b/src/screen/dashboard/mod.rs @@ -0,0 +1,43 @@ +mod screen; +mod titlebar; + +use gpui::{AppContext, BorrowAppContext, point, px, size}; +pub(crate) use screen::new; + +use crate::{app, screen::dashboard::screen::Screen}; + +pub fn open_window(screen: Screen, cx: &mut gpui::App) -> anyhow::Result<()> { + let (top_left, window_bounds) = cx.read_global::(|global, cx| { + ( + global.safe_area.origin, + gpui::Bounds::centered(None, size(px(800.), px(600.0)), cx), + ) + }); + + cx.open_window( + gpui::WindowOptions { + window_bounds: Some(gpui::WindowBounds::Windowed(window_bounds)), + titlebar: Some(gpui::TitlebarOptions { + appears_transparent: true, + traffic_light_position: Some(top_left + point(px(12.), px(12.))), + ..Default::default() + }), + ..Default::default() + }, + |window, cx| { + cx.new(|cx| { + cx.observe_window_appearance(window, |_, window, cx| { + cx.update_global::(|global, cx| { + global.current_theme = global + .theme_family + .theme_for_appearance(window.appearance()); + cx.notify(); + }); + }) + .detach(); + screen + }) + }, + ) + .map(|_| ()) +} diff --git a/src/screen/dashboard/screen.rs b/src/screen/dashboard/screen.rs new file mode 100644 index 0000000..8b2331e --- /dev/null +++ b/src/screen/dashboard/screen.rs @@ -0,0 +1,41 @@ +use gpui::{AppContext, ParentElement, Styled, div}; + +use crate::{app, screen::dashboard::titlebar}; + +pub(crate) struct Screen { + titlebar: gpui::Entity, +} + +pub(crate) fn new(cx: &mut gpui::App) -> Screen { + Screen { + titlebar: cx.new(|cx| titlebar::new(cx)), + } +} + +impl gpui::Render for Screen { + fn render( + &mut self, + _window: &mut gpui::Window, + cx: &mut gpui::Context, + ) -> impl gpui::IntoElement { + let theme = app::current_theme(cx); + div() + .flex() + .flex_col() + .bg(theme.colors.background) + .size_full() + .child(self.titlebar.clone()) + .child( + div() + .flex() + .flex_row() + .flex_1() + .w_full() + .gap_2() + .px_3() + .pb_3() + .child(div().w_1_4().h_full().rounded_lg().bg(theme.colors.surface)) + .child(div().w_3_4().h_full().rounded_lg().bg(theme.colors.surface)), + ) + } +} diff --git a/src/titlebar.rs b/src/screen/dashboard/titlebar.rs similarity index 90% rename from src/titlebar.rs rename to src/screen/dashboard/titlebar.rs index 336abe5..273e353 100644 --- a/src/titlebar.rs +++ b/src/screen/dashboard/titlebar.rs @@ -1,4 +1,4 @@ -use gpui::{ParentElement, Styled, div}; +use gpui::{ParentElement, Styled, TitlebarOptions, div}; use crate::component::button::button; use crate::query::{self, QueryStatus, read_query, use_query}; @@ -16,11 +16,9 @@ pub struct TitleBar { pub struct RepoSelector {} -impl TitleBar { - pub fn new(cx: &mut gpui::Context) -> Self { - Self { - fetch_user_query: use_query(api::user::Fetch, cx), - } +pub fn new(cx: &mut gpui::Context) -> TitleBar { + TitleBar { + fetch_user_query: use_query(api::user::Fetch, cx), } } diff --git a/src/screen/mod.rs b/src/screen/mod.rs index 1411cc0..eaba9b1 100644 --- a/src/screen/mod.rs +++ b/src/screen/mod.rs @@ -1 +1,2 @@ +pub(crate) mod dashboard; pub(crate) mod setup_wizard; diff --git a/src/screen/setup_wizard/github_step.rs b/src/screen/setup_wizard/github_step.rs index 2ddda33..66bfcde 100644 --- a/src/screen/setup_wizard/github_step.rs +++ b/src/screen/setup_wizard/github_step.rs @@ -2,8 +2,8 @@ use std::time::Duration; use futures_lite::StreamExt; use gpui::{ - BorrowAppContext, InteractiveElement, ParentElement, Styled, Timer, div, img, - prelude::FluentBuilder, + BorrowAppContext, InteractiveElement, ParentElement, StatefulInteractiveElement, Styled, Timer, + div, img, prelude::FluentBuilder, }; use rand::RngExt; @@ -62,28 +62,28 @@ impl GithubStepView { fn on_create(&mut self, cx: &mut gpui::Context) { cx.observe(&self.create_device_code_query, |this, _, cx| { - let code = { + let codes = { let data = read_query(&this.create_device_code_query, cx); if let QueryStatus::Loaded(data) = data { - Some(data.device_code.clone()) + Some((data.device_code.clone(), data.user_code.clone())) } else { None } }; - if let Some(ref code) = code + if let Some((ref device_code, ref user_code)) = codes && !this.has_opened_link && !this.is_opening_link { this.is_opening_link = true; - this.copy_device_code(code, cx); + this.copy_user_code(user_code, cx); - let code = code.clone(); + let device_code = device_code.clone(); set_timeout( move |weak, cx| { _ = weak.update(cx, |this, cx| { this.has_opened_link = true; this.is_opening_link = false; - this.begin_auth_flow(&code, cx); + this.begin_auth_flow(&device_code, cx); cx.notify(); }); }, @@ -137,7 +137,7 @@ impl GithubStepView { .collect() } - fn copy_device_code(&mut self, code: &str, cx: &mut gpui::Context) { + fn copy_user_code(&mut self, code: &str, cx: &mut gpui::Context) { cx.write_to_clipboard(gpui::ClipboardItem::new_string(code.to_owned())); self.has_copied_code = true; @@ -262,9 +262,9 @@ impl GithubStepView { let theme = app::current_theme(cx); - let displayed_code = match create_device_code_query { - QueryStatus::Loaded(data) => &data.user_code, - _ => &self.placeholder_code, + let (displayed_code, copyable_code) = match create_device_code_query { + QueryStatus::Loaded(data) => (data.user_code.as_str(), Some(data.user_code.clone())), + _ => (self.placeholder_code.as_str(), None), }; let border_color = theme.colors.border.clone(); @@ -304,6 +304,12 @@ impl GithubStepView { .items_center() .justify_center() .gap_1p5() + .when_some(copyable_code, |it, code| { + it.cursor_pointer() + .on_click(cx.listener(move |this, _, _, cx| { + this.copy_user_code(&code, cx); + })) + }) .children(letter_boxes), ) .child( diff --git a/src/screen/setup_wizard/screen.rs b/src/screen/setup_wizard/screen.rs index af994e1..932b2d3 100644 --- a/src/screen/setup_wizard/screen.rs +++ b/src/screen/setup_wizard/screen.rs @@ -1,4 +1,4 @@ -use gpui::{AppContext, IntoElement, ParentElement, Styled, div, prelude::FluentBuilder}; +use gpui::{AppContext, IntoElement, ParentElement, Styled, div, prelude::FluentBuilder, rems}; use crate::{ api, app, @@ -120,7 +120,8 @@ impl Screen { .flex_col() .items_start() .w_full() - .px_8() + .pl_6() + .pr_8() .justify_center() .gap_3() .text_sm() @@ -165,9 +166,17 @@ impl gpui::Render for Screen { .bg(theme.colors.surface) .relative() .child( - text("Novem") - .bold() - .styled(|it| it.absolute().top_20().left_8()), + div() + .flex() + .flex_row() + .justify_center() + .items_center() + .gap_2p5() + .absolute() + .top_20() + .left_6() + .child(font_icon(FontIcon::Cat).size_4()) + .child(text("Novem").bold()), ) .child(self.step_list(cx)), ) diff --git a/src/storage.rs b/src/storage.rs index d4d93f0..cbe8569 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -6,6 +6,9 @@ use crate::api; #[derive(Default, Serialize, Deserialize)] pub(crate) struct PersistedState { pub selected_account: api::user::Id, + + #[cfg(debug_assertions)] + pub debug_auth_tokens: Option, } pub(crate) fn data_dir_path() -> std::path::PathBuf { @@ -47,11 +50,17 @@ pub(crate) async fn load_auth_tokens( cx: &gpui::App, user_id: api::user::Id, ) -> Option { - cx.read_credentials(&format!("https://github.com/user/{}", user_id)) - .await - .ok()? - .and_then(|(_, password)| String::from_utf8(password).ok()) - .map(|access_token| api::AuthTokens { access_token }) + if cfg!(debug_assertions) { + // in debug mode, credentials are loaded from persisted state + // to avoid being prompted for permission to access keychain on macos + None + } else { + cx.read_credentials(&format!("https://github.com/user/{}", user_id)) + .await + .ok()? + .and_then(|(_, password)| String::from_utf8(password).ok()) + .map(|access_token| api::AuthTokens { access_token }) + } } pub(crate) fn store_auth_tokens( @@ -59,9 +68,16 @@ pub(crate) fn store_auth_tokens( user: &api::user::User, cx: &gpui::App, ) -> gpui::Task> { - cx.write_credentials( - &format!("https://github.com/user/{}", user.id), - &format!("{}", user.id), - tokens.access_token.as_bytes(), - ) + if cfg!(debug_assertions) { + let r = update_persisted_state(|state| { + state.debug_auth_tokens = Some(tokens.clone()); + }); + gpui::Task::ready(r) + } else { + cx.write_credentials( + &format!("https://github.com/user/{}", user.id), + &format!("{}", user.id), + tokens.access_token.as_bytes(), + ) + } }