From 2c3de1fd6ed8f264dfe1f54a9b9173c57494dc14 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 13 May 2026 20:02:26 +0800 Subject: [PATCH] refactor: redesign theme tokens and split catppuccin themes --- build.rs | 10 +- src/api.rs | 4 +- src/api/issues.rs | 190 ++++----- src/api/mock.rs | 22 +- src/colors.rs | 107 ++--- src/component/button.rs | 10 +- src/component/markdown.rs | 456 +++++++++++----------- src/main.rs | 12 +- src/query.rs | 18 +- src/screen/dashboard/issue_list.rs | 100 +++-- src/screen/dashboard/pull_request_view.rs | 168 ++++---- src/screen/dashboard/screen.rs | 40 +- src/screen/dashboard/sidebar.rs | 14 +- src/screen/dashboard/titlebar.rs | 12 +- src/screen/setup_wizard/github_step.rs | 28 +- src/screen/setup_wizard/mod.rs | 6 +- src/screen/setup_wizard/screen.rs | 28 +- src/storage.rs | 4 +- src/theme.rs | 114 +++--- src/theme/catppuccin.rs | 121 ++++++ 20 files changed, 797 insertions(+), 667 deletions(-) create mode 100644 src/theme/catppuccin.rs diff --git a/build.rs b/build.rs index d1af8ed..473fb70 100644 --- a/build.rs +++ b/build.rs @@ -229,8 +229,8 @@ fn render_github_fixtures(fixture_root: &Path) -> String { output.push_str(&string_literal(&id)); output.push_str(", "); match previous_end_cursor.as_deref() { - Some(after) => output.push_str(&format!("Some({})", string_literal(after))), - None => output.push_str("None"), + | Some(after) => output.push_str(&format!("Some({})", string_literal(after))), + | None => output.push_str("None"), } output.push_str(") => Some("); output.push_str(&string_literal(&fixture.json)); @@ -343,9 +343,9 @@ fn issue_fixture_state(issue: &serde_json::Value) -> &'static str { } match required_string(issue, &["state"]) { - "open" => "OPEN", - "closed" => "CLOSED", - state => panic!("unsupported pull request state in fixture: {state}"), + | "open" => "OPEN", + | "closed" => "CLOSED", + | state => panic!("unsupported pull request state in fixture: {state}"), } } diff --git a/src/api.rs b/src/api.rs index 143415f..f35bd9b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -136,7 +136,7 @@ where { let mut body: graphql_client::Response = res.json().await?; match body.data.take() { - None => Err(Error::GraphQLError(body.errors.unwrap_or_default())), - Some(data) => Ok((body, data)), + | None => Err(Error::GraphQLError(body.errors.unwrap_or_default())), + | Some(data) => Ok((body, data)), } } diff --git a/src/api/issues.rs b/src/api/issues.rs index 59a85c7..5488145 100644 --- a/src/api/issues.rs +++ b/src/api/issues.rs @@ -289,8 +289,8 @@ impl query::QueryFn for ListPullRequests { } let query_string = match self.filter { - | Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter), - | None => "is:pr archived:false sort:updated-desc".into(), + | Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter), + | None => "is:pr archived:false sort:updated-desc".into(), }; let gql = @@ -312,19 +312,19 @@ impl query::QueryFn for ListPullRequests { .flatten() .filter_map(|edge| { edge.node.and_then(|n| match n { - | PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => { - Some(PullRequest { - id: p.id.into(), - title: p.title, - state: p.state, - is_draft: p.is_draft, - repo_slug: format!( - "{}/{}", - p.repository.owner.login, p.repository.name - ), - }) - } - | _ => None, + | PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => { + Some(PullRequest { + id: p.id.into(), + title: p.title, + state: p.state, + is_draft: p.is_draft, + repo_slug: format!( + "{}/{}", + p.repository.owner.login, p.repository.name + ), + }) + } + | _ => None, }) }) .collect::>() @@ -369,32 +369,32 @@ impl query::QueryFn for FetchPullRequest { "missing 'node' field on PullRequestQuery response".into(), )) .and_then(|n| match n { - | PullRequestQueryNode::PullRequest(p) => { - let created_at = - chrono::DateTime::parse_from_rfc3339(&p.created_at).map_err(|err| { - api::Error::MalformedResponse(format!( - "invalid pull request createdAt {:?}: {err}", - p.created_at - )) - })?; + | PullRequestQueryNode::PullRequest(p) => { + let created_at = + chrono::DateTime::parse_from_rfc3339(&p.created_at).map_err(|err| { + api::Error::MalformedResponse(format!( + "invalid pull request createdAt {:?}: {err}", + p.created_at + )) + })?; - Ok(DetailedPullRequest { - title: p.title, - state: p.state, - is_draft: p.is_draft, - body: p.body, - author: p.author.map(|it| api::user::Actor { - login: it.login, - avatar_url: it.avatar_url, - }), - base_branch_name: p.base_ref.map(|r| r.name), - head_branch_name: p.head_ref.map(|r| r.name), - created_at: Some(created_at), - }) - } - | _ => Err(api::Error::MalformedResponse( - "unexpected node type on PullRequestQuery".into(), - )), + Ok(DetailedPullRequest { + title: p.title, + state: p.state, + is_draft: p.is_draft, + body: p.body, + author: p.author.map(|it| api::user::Actor { + login: it.login, + avatar_url: it.avatar_url, + }), + base_branch_name: p.base_ref.map(|r| r.name), + head_branch_name: p.head_ref.map(|r| r.name), + created_at: Some(created_at), + }) + } + | _ => Err(api::Error::MalformedResponse( + "unexpected node type on PullRequestQuery".into(), + )), }) } } @@ -437,11 +437,11 @@ impl query::QueryFn for FetchPullRequestTimeline { TimelineActor { kind: match on { - | actorFieldsOn::Bot => "Bot", - | actorFieldsOn::EnterpriseUserAccount => "EnterpriseUserAccount", - | actorFieldsOn::Mannequin => "Mannequin", - | actorFieldsOn::Organization => "Organization", - | actorFieldsOn::User => "User", + | actorFieldsOn::Bot => "Bot", + | actorFieldsOn::EnterpriseUserAccount => "EnterpriseUserAccount", + | actorFieldsOn::Mannequin => "Mannequin", + | actorFieldsOn::Organization => "Organization", + | actorFieldsOn::User => "User", } .into(), name: login, @@ -451,62 +451,62 @@ impl query::QueryFn for FetchPullRequestTimeline { fn normalize_assignee(actor: assigneeFields) -> TimelineActor { match actor { - | assigneeFields::Bot(actor) => TimelineActor { - kind: "Bot".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), - }, - | assigneeFields::Mannequin(actor) => TimelineActor { - kind: "Mannequin".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), - }, - | assigneeFields::Organization(actor) => TimelineActor { - kind: "Organization".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), - }, - | assigneeFields::User(actor) => TimelineActor { - kind: "User".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), - }, + | assigneeFields::Bot(actor) => TimelineActor { + kind: "Bot".into(), + name: actor.login, + avatar_url: Some(actor.avatar_url), + }, + | assigneeFields::Mannequin(actor) => TimelineActor { + kind: "Mannequin".into(), + name: actor.login, + avatar_url: Some(actor.avatar_url), + }, + | assigneeFields::Organization(actor) => TimelineActor { + kind: "Organization".into(), + name: actor.login, + avatar_url: Some(actor.avatar_url), + }, + | assigneeFields::User(actor) => TimelineActor { + kind: "User".into(), + name: actor.login, + avatar_url: Some(actor.avatar_url), + }, } } fn normalize_requested_reviewer(actor: requestedReviewerFields) -> TimelineActor { match actor { - | requestedReviewerFields::Bot(actor) => TimelineActor { - kind: "Bot".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), - }, - | requestedReviewerFields::Mannequin(actor) => TimelineActor { - kind: "Mannequin".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), - }, - | requestedReviewerFields::Team(actor) => TimelineActor { - kind: "Team".into(), - name: actor.name, - avatar_url: None, - }, - | requestedReviewerFields::User(actor) => TimelineActor { - kind: "User".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), - }, + | requestedReviewerFields::Bot(actor) => TimelineActor { + kind: "Bot".into(), + name: actor.login, + avatar_url: Some(actor.avatar_url), + }, + | requestedReviewerFields::Mannequin(actor) => TimelineActor { + kind: "Mannequin".into(), + name: actor.login, + avatar_url: Some(actor.avatar_url), + }, + | requestedReviewerFields::Team(actor) => TimelineActor { + kind: "Team".into(), + name: actor.name, + avatar_url: None, + }, + | requestedReviewerFields::User(actor) => TimelineActor { + kind: "User".into(), + name: actor.login, + avatar_url: Some(actor.avatar_url), + }, } } fn normalize_review_state(state: PullRequestReviewState) -> String { match state { - | PullRequestReviewState::PENDING => "PENDING", - | PullRequestReviewState::COMMENTED => "COMMENTED", - | PullRequestReviewState::APPROVED => "APPROVED", - | PullRequestReviewState::CHANGES_REQUESTED => "CHANGES_REQUESTED", - | PullRequestReviewState::DISMISSED => "DISMISSED", - | _ => "OTHER", + | PullRequestReviewState::PENDING => "PENDING", + | PullRequestReviewState::COMMENTED => "COMMENTED", + | PullRequestReviewState::APPROVED => "APPROVED", + | PullRequestReviewState::CHANGES_REQUESTED => "CHANGES_REQUESTED", + | PullRequestReviewState::DISMISSED => "DISMISSED", + | _ => "OTHER", } .into() } @@ -726,10 +726,10 @@ impl query::QueryFn for FetchPullRequestTimeline { "missing 'node' field on PullRequestTimelineQuery response".into(), )) .and_then(|node| match node { - | PullRequestTimelineQueryNode::PullRequest(pull_request) => Ok(pull_request), - | _ => Err(api::Error::MalformedResponse( - "unexpected node type on PullRequestTimelineQuery".into(), - )), + | PullRequestTimelineQueryNode::PullRequest(pull_request) => Ok(pull_request), + | _ => Err(api::Error::MalformedResponse( + "unexpected node type on PullRequestTimelineQuery".into(), + )), })?; let timeline = pull_request.timeline_items; diff --git a/src/api/mock.rs b/src/api/mock.rs index 7fd2d6e..a4a4878 100644 --- a/src/api/mock.rs +++ b/src/api/mock.rs @@ -167,7 +167,10 @@ mod tests { .map(|author| author.login.as_str()), Some("kennethnym") ); - assert_eq!(documented_failover.base_branch_name.as_deref(), Some("main")); + assert_eq!( + documented_failover.base_branch_name.as_deref(), + Some("main") + ); assert_eq!( documented_failover.head_branch_name.as_deref(), Some("docs/manual-failover-steps") @@ -203,7 +206,10 @@ mod tests { Some(chrono::DateTime::parse_from_rfc3339("2026-05-03T07:40:00Z").unwrap()) ); assert_eq!( - worker_split.author.as_ref().map(|author| author.login.as_str()), + worker_split + .author + .as_ref() + .map(|author| author.login.as_str()), Some("leaferiksen") ); assert_eq!(worker_split.base_branch_name.as_deref(), Some("main")); @@ -295,36 +301,36 @@ mod tests { .expect("third timeline fixture json should parse"); let first_page_nodes = match first_page.node.as_ref() { - Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => { + | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => { pull_request .timeline_items .nodes .as_ref() .expect("first timeline fixture page should contain timeline nodes") } - _ => panic!("first timeline fixture page should resolve to a pull request node"), + | _ => panic!("first timeline fixture page should resolve to a pull request node"), }; let second_page_nodes = match second_page.node.as_ref() { - Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => { + | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => { pull_request .timeline_items .nodes .as_ref() .expect("second timeline fixture page should contain timeline nodes") } - _ => panic!("second timeline fixture page should resolve to a pull request node"), + | _ => panic!("second timeline fixture page should resolve to a pull request node"), }; let third_page_nodes = match third_page.node.as_ref() { - Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => { + | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => { pull_request .timeline_items .nodes .as_ref() .expect("third timeline fixture page should contain timeline nodes") } - _ => panic!("third timeline fixture page should resolve to a pull request node"), + | _ => panic!("third timeline fixture page should resolve to a pull request node"), }; assert_eq!( diff --git a/src/colors.rs b/src/colors.rs index 0c9c010..9542a00 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -11,74 +11,85 @@ pub const fn hex(hex: u32) -> Rgba { } } +pub const fn hex_alpha(hex: u32, alpha: f32) -> Rgba { + let [_, r, g, b] = hex.to_be_bytes(); + + Rgba { + r: r as f32 / 255.0, + g: g as f32 / 255.0, + b: b as f32 / 255.0, + a: alpha, + } +} + #[allow(dead_code)] pub const fn neutral(shade: u16) -> Rgba { match shade { - 50 => hex(0xfafafa), - 100 => hex(0xf5f5f5), - 200 => hex(0xe5e5e5), - 300 => hex(0xd4d4d4), - 400 => hex(0xa3a3a3), - 500 => hex(0x737373), - 600 => hex(0x525252), - 700 => hex(0x404040), - 800 => hex(0x262626), - 900 => hex(0x171717), - 950 => hex(0x0a0a0a), - _ => panic!("unsupported Tailwind neutral shade"), + | 50 => hex(0xfafafa), + | 100 => hex(0xf5f5f5), + | 200 => hex(0xe5e5e5), + | 300 => hex(0xd4d4d4), + | 400 => hex(0xa3a3a3), + | 500 => hex(0x737373), + | 600 => hex(0x525252), + | 700 => hex(0x404040), + | 800 => hex(0x262626), + | 900 => hex(0x171717), + | 950 => hex(0x0a0a0a), + | _ => panic!("unsupported Tailwind neutral shade"), } } #[allow(dead_code)] pub const fn violet(shade: u16) -> Rgba { match shade { - 50 => hex(0xf5f3ff), - 100 => hex(0xede9fe), - 200 => hex(0xddd6fe), - 300 => hex(0xc4b5fd), - 400 => hex(0xa78bfa), - 500 => hex(0x8b5cf6), - 600 => hex(0x7c3aed), - 700 => hex(0x6d28d9), - 800 => hex(0x5b21b6), - 900 => hex(0x4c1d95), - 950 => hex(0x2e1065), - _ => panic!("unsupported Tailwind violet shade"), + | 50 => hex(0xf5f3ff), + | 100 => hex(0xede9fe), + | 200 => hex(0xddd6fe), + | 300 => hex(0xc4b5fd), + | 400 => hex(0xa78bfa), + | 500 => hex(0x8b5cf6), + | 600 => hex(0x7c3aed), + | 700 => hex(0x6d28d9), + | 800 => hex(0x5b21b6), + | 900 => hex(0x4c1d95), + | 950 => hex(0x2e1065), + | _ => panic!("unsupported Tailwind violet shade"), } } #[allow(dead_code)] pub const fn amber(shade: u16) -> Rgba { match shade { - 50 => hex(0xfffbeb), - 100 => hex(0xfef3c7), - 200 => hex(0xfde68a), - 300 => hex(0xfcd34d), - 400 => hex(0xfbbf24), - 500 => hex(0xf59e0b), - 600 => hex(0xd97706), - 700 => hex(0xb45309), - 800 => hex(0x92400e), - 900 => hex(0x78350f), - 950 => hex(0x451a03), - _ => panic!("unsupported Tailwind amber shade"), + | 50 => hex(0xfffbeb), + | 100 => hex(0xfef3c7), + | 200 => hex(0xfde68a), + | 300 => hex(0xfcd34d), + | 400 => hex(0xfbbf24), + | 500 => hex(0xf59e0b), + | 600 => hex(0xd97706), + | 700 => hex(0xb45309), + | 800 => hex(0x92400e), + | 900 => hex(0x78350f), + | 950 => hex(0x451a03), + | _ => panic!("unsupported Tailwind amber shade"), } } #[allow(dead_code)] pub const fn red(shade: u16) -> Rgba { match shade { - 50 => hex(0xfef2f2), - 100 => hex(0xfee2e2), - 200 => hex(0xfecaca), - 300 => hex(0xfca5a5), - 400 => hex(0xf87171), - 500 => hex(0xef4444), - 600 => hex(0xdc2626), - 700 => hex(0xb91c1c), - 800 => hex(0x991b1b), - 900 => hex(0x7f1d1d), - 950 => hex(0x450a0a), - _ => panic!("unsupported Tailwind red shade"), + | 50 => hex(0xfef2f2), + | 100 => hex(0xfee2e2), + | 200 => hex(0xfecaca), + | 300 => hex(0xfca5a5), + | 400 => hex(0xf87171), + | 500 => hex(0xef4444), + | 600 => hex(0xdc2626), + | 700 => hex(0xb91c1c), + | 800 => hex(0x991b1b), + | 900 => hex(0x7f1d1d), + | 950 => hex(0x450a0a), + | _ => panic!("unsupported Tailwind red shade"), } } diff --git a/src/component/button.rs b/src/component/button.rs index 4569404..74132a8 100644 --- a/src/component/button.rs +++ b/src/component/button.rs @@ -87,8 +87,8 @@ impl gpui::RenderOnce for Button { let theme = app::current_theme(cx); let icon_color = match self.variant { - | Variant::Primary => theme.colors.accent_text, - | Variant::Secondary => theme.colors.text, + | Variant::Primary => theme.colors.accent_on_solid, + | Variant::Secondary => theme.colors.text, }; let mut children: Vec = Vec::with_capacity(3); @@ -115,10 +115,10 @@ impl gpui::RenderOnce for Button { .py_0p5() .children(children) .when(matches!(self.variant, Variant::Primary), |div| { - div.bg(theme.colors.accent) - .text_color(theme.colors.accent_text) + div.bg(theme.colors.accent_solid) + .text_color(theme.colors.accent_on_solid) .border_1() - .border_color(theme.colors.border.blend(theme.colors.accent)) + .border_color(theme.colors.border.blend(theme.colors.accent_solid)) }) .when(matches!(self.variant, Variant::Secondary), |div| { div.bg(theme.colors.surface_elevated) diff --git a/src/component/markdown.rs b/src/component/markdown.rs index 6f0dbd9..49f3ce3 100644 --- a/src/component/markdown.rs +++ b/src/component/markdown.rs @@ -115,7 +115,7 @@ pub(crate) fn new( impl Styled for ContentBlock { fn style(&mut self) -> &mut gpui::StyleRefinement { match self { - | ContentBlock::Text { style, .. } => style, + | ContentBlock::Text { style, .. } => style, } } } @@ -179,54 +179,56 @@ impl MarkdownText { } match node.kind_id() { - | MARKDOWN_KIND_ID_EMPHASIS => { - highlights.push(( - node_range!(), - gpui::HighlightStyle { - font_style: Some(gpui::FontStyle::Italic), - ..Default::default() - }, - )); - } - | MARKDOWN_KIND_ID_STRONG_EMPHASIS => highlights.push(( - node_range!(), - gpui::HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - )), - - | MARKDOWN_KIND_ID_LINK => { - if cursor.goto_first_child() { + | MARKDOWN_KIND_ID_EMPHASIS => { highlights.push(( node_range!(), gpui::HighlightStyle { - color: Some(theme.colors.accent.into()), - underline: Some(gpui::UnderlineStyle { - color: Some(theme.colors.accent.into()), - thickness: px(1.), - wavy: false, - }), + font_style: Some(gpui::FontStyle::Italic), ..Default::default() }, )); + } + | MARKDOWN_KIND_ID_STRONG_EMPHASIS => highlights.push(( + node_range!(), + gpui::HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + ..Default::default() + }, + )), - if cursor.goto_next_sibling() - && let Ok(src) = cursor.node().utf8_text(content.as_bytes()) - { - links - .push((node_range!(), gpui::SharedString::from(String::from(src)))); - } else { - // the link src is invalid, use an empty string as a fallback - // link on click handler will ignore empty string - links.push((node_range!(), "".into())) + | MARKDOWN_KIND_ID_LINK => { + if cursor.goto_first_child() { + highlights.push(( + node_range!(), + gpui::HighlightStyle { + color: Some(theme.colors.link.into()), + underline: Some(gpui::UnderlineStyle { + color: Some(theme.colors.link.into()), + thickness: px(1.), + wavy: false, + }), + ..Default::default() + }, + )); + + if cursor.goto_next_sibling() + && let Ok(src) = cursor.node().utf8_text(content.as_bytes()) + { + links.push(( + node_range!(), + gpui::SharedString::from(String::from(src)), + )); + } else { + // the link src is invalid, use an empty string as a fallback + // link on click handler will ignore empty string + links.push((node_range!(), "".into())) + } } } - } - | _ => { - // extend here to support more markdown node stylings - } + | _ => { + // extend here to support more markdown node stylings + } }; if !cursor.goto_next_sibling() { @@ -303,23 +305,23 @@ impl MarkdownText { let marker_content = &content[marker_node.byte_range()]; let list_marker_char = match marker_content { - // unordered list item - | "-" | "+" | "*" => Some("•".to_string()), + // unordered list item + | "-" | "+" | "*" => Some("•".to_string()), - | marker_content if ORDERED_LIST_MARKER_REGEX.is_match(marker_content) => { - let i = list_index.get_or_insert_with(|| { - marker_content - .strip_suffix('.') - .unwrap() - .parse::() - .unwrap() - }); - let j = *i; - *i = j + 1; - Some(format!("{j}.")) - } + | marker_content if ORDERED_LIST_MARKER_REGEX.is_match(marker_content) => { + let i = list_index.get_or_insert_with(|| { + marker_content + .strip_suffix('.') + .unwrap() + .parse::() + .unwrap() + }); + let j = *i; + *i = j + 1; + Some(format!("{j}.")) + } - | _ => None, + | _ => None, }; let Some(list_marker_char) = list_marker_char else { @@ -331,9 +333,9 @@ impl MarkdownText { let block = if cursor.goto_next_sibling() { let mut b = block_for_node(cursor, content, 0, theme); match b { - | ContentBlock::Text { - ref mut decoration, .. - } => *decoration = Some(list_marker_char.into()), + | ContentBlock::Text { + ref mut decoration, .. + } => *decoration = Some(list_marker_char.into()), } b } else { @@ -372,150 +374,150 @@ impl MarkdownText { } match current_node.kind_id() { - | MARKDOWN_KIND_ID_ATX_HEADING => { - if !cursor.goto_first_child() { - render_fallback_content(&cursor, &self.content, &mut self.blocks); - continue; - } - - let marker_node_kind = cursor.node().kind_id(); - - let block = if cursor.goto_next_sibling() - && cursor.node().kind_id() == MARKDOWN_KIND_ID_HEADING_CONTENT - { - // because HEADING_CONTENT node includes the space after the heading marker - // offset by 1 to exclude the space - block_for_node(&mut cursor, &self.content, 1, theme) - } else { - ContentBlock::Text { - decoration: None, - text: gpui::SharedString::new(&self.content[current_node.byte_range()]), - highlights: Vec::new(), - links: Vec::new(), - style: gpui::StyleRefinement::default(), + | MARKDOWN_KIND_ID_ATX_HEADING => { + if !cursor.goto_first_child() { + render_fallback_content(&cursor, &self.content, &mut self.blocks); + continue; } - }; - let mut block = match marker_node_kind { - | MARKDOWN_KIND_ID_ATX_H1_MARKER => block - .text_size(rems(2.25)) - .font_weight(gpui::FontWeight::EXTRA_BOLD) - .mb_6(), - | MARKDOWN_KIND_ID_ATX_H2_MARKER => block - .text_2xl() - .font_weight(gpui::FontWeight::BOLD) - .mt_12() - .mb_4(), - | MARKDOWN_KIND_ID_ATX_H3_MARKER => block - .text_xl() - .font_weight(gpui::FontWeight::SEMIBOLD) - .mt_8() - .mb_3(), - | MARKDOWN_KIND_ID_ATX_H4_MARKER => block - .text_base() - .font_weight(gpui::FontWeight::SEMIBOLD) - .mt_6() - .mb_2(), - | _ => block, - } - .text_color(theme.colors.text); + let marker_node_kind = cursor.node().kind_id(); - if is_first_heading { - is_first_heading = false; - block = block.mt_0(); + let block = if cursor.goto_next_sibling() + && cursor.node().kind_id() == MARKDOWN_KIND_ID_HEADING_CONTENT + { + // because HEADING_CONTENT node includes the space after the heading marker + // offset by 1 to exclude the space + block_for_node(&mut cursor, &self.content, 1, theme) + } else { + ContentBlock::Text { + decoration: None, + text: gpui::SharedString::new(&self.content[current_node.byte_range()]), + highlights: Vec::new(), + links: Vec::new(), + style: gpui::StyleRefinement::default(), + } + }; + + let mut block = match marker_node_kind { + | MARKDOWN_KIND_ID_ATX_H1_MARKER => block + .text_size(rems(2.25)) + .font_weight(gpui::FontWeight::EXTRA_BOLD) + .mb_6(), + | MARKDOWN_KIND_ID_ATX_H2_MARKER => block + .text_2xl() + .font_weight(gpui::FontWeight::BOLD) + .mt_12() + .mb_4(), + | MARKDOWN_KIND_ID_ATX_H3_MARKER => block + .text_xl() + .font_weight(gpui::FontWeight::SEMIBOLD) + .mt_8() + .mb_3(), + | MARKDOWN_KIND_ID_ATX_H4_MARKER => block + .text_base() + .font_weight(gpui::FontWeight::SEMIBOLD) + .mt_6() + .mb_2(), + | _ => block, + } + .text_color(theme.colors.text); + + if is_first_heading { + is_first_heading = false; + block = block.mt_0(); + } + + cursor.goto_parent(); + + self.blocks.push(block); } - cursor.goto_parent(); + | MARKDOWN_KIND_ID_PARAGRAPH => { + let block = block_for_node(&mut cursor, &self.content, 0, theme) + .text_color(theme.colors.text) + .text_sm(); - self.blocks.push(block); - } - - | MARKDOWN_KIND_ID_PARAGRAPH => { - let block = block_for_node(&mut cursor, &self.content, 0, theme) - .text_color(theme.colors.text) - .text_sm(); - - self.blocks.push(block); - } - - | MARKDOWN_KIND_ID_TIGHT_LIST => { - let is_rendered = - render_list_node(&mut cursor, &self.content, &mut self.blocks, theme, 0); - if !is_rendered { - continue; - } - } - - | MARKDOWN_KIND_ID_FENCED_CODE_BLOCK => { - // expected tree shape: - // fenced_code_block - // ├── info_string? (present if there is a language annotation) - // └── code_fence_content? (present if there is some content between the backticks) - - if !cursor.goto_first_child() { - render_fallback_content(&cursor, &self.content, &mut self.blocks); - continue; + self.blocks.push(block); } - let content = if cursor.node().kind_id() == MARKDOWN_KIND_ID_INFO_STRING { - // skipping info string (which annotates the code block) - if cursor.goto_next_sibling() { - // this is code_fence_content node + | MARKDOWN_KIND_ID_TIGHT_LIST => { + let is_rendered = + render_list_node(&mut cursor, &self.content, &mut self.blocks, theme, 0); + if !is_rendered { + continue; + } + } + + | MARKDOWN_KIND_ID_FENCED_CODE_BLOCK => { + // expected tree shape: + // fenced_code_block + // ├── info_string? (present if there is a language annotation) + // └── code_fence_content? (present if there is some content between the backticks) + + if !cursor.goto_first_child() { + render_fallback_content(&cursor, &self.content, &mut self.blocks); + continue; + } + + let content = if cursor.node().kind_id() == MARKDOWN_KIND_ID_INFO_STRING { + // skipping info string (which annotates the code block) + if cursor.goto_next_sibling() { + // this is code_fence_content node + gpui::SharedString::new( + cursor + .node() + .utf8_text(self.content.as_bytes()) + .unwrap_or_default(), + ) + } else { + gpui::SharedString::default() + } + } else { + // assuming the current node is already code_fence_content gpui::SharedString::new( cursor .node() .utf8_text(self.content.as_bytes()) .unwrap_or_default(), ) - } else { - gpui::SharedString::default() + }; + + cursor.goto_parent(); + + let block = ContentBlock::Text { + decoration: None, + text: content, + highlights: Vec::new(), + links: Vec::new(), + style: gpui::StyleRefinement::default(), } - } else { - // assuming the current node is already code_fence_content - gpui::SharedString::new( - cursor - .node() - .utf8_text(self.content.as_bytes()) - .unwrap_or_default(), - ) - }; - - cursor.goto_parent(); - - let block = ContentBlock::Text { - decoration: None, - text: content, - highlights: Vec::new(), - links: Vec::new(), - style: gpui::StyleRefinement::default(), - } - .text_sm() - .text_color(theme.colors.text) - .line_height(relative(1.2)) - .font_family("Menlo") - .px_3() - .py_2() - .rounded_sm() - .bg(theme.colors.surface) - .border_1() - .my_4() - .border_color(theme.colors.border); - - self.blocks.push(block); - } - - | _ => { - println!( - "[WARN] formatting not implemenetd for node type {:?}", - current_node.kind() - ); - - let block = block_for_node(&mut cursor, &self.content, 0, theme) + .text_sm() .text_color(theme.colors.text) - .text_sm(); + .line_height(relative(1.2)) + .font_family("Menlo") + .px_3() + .py_2() + .rounded_sm() + .bg(theme.colors.code_bg) + .border_1() + .my_4() + .border_color(theme.colors.code_border); - self.blocks.push(block); - } + self.blocks.push(block); + } + + | _ => { + println!( + "[WARN] formatting not implemenetd for node type {:?}", + current_node.kind() + ); + + let block = block_for_node(&mut cursor, &self.content, 0, theme) + .text_color(theme.colors.text) + .text_sm(); + + self.blocks.push(block); + } } if !cursor.goto_next_sibling() { @@ -533,55 +535,55 @@ impl gpui::Render for MarkdownText { ) -> impl gpui::prelude::IntoElement { let children = self.blocks.iter().enumerate().map(|(i, block)| { match block { - | ContentBlock::Text { - decoration, - text, - highlights, - links, - style, - } => { - let styled_text = - gpui::StyledText::new(text.clone()).with_highlights(highlights.clone()); + | ContentBlock::Text { + decoration, + text, + highlights, + links, + style, + } => { + let styled_text = + gpui::StyledText::new(text.clone()).with_highlights(highlights.clone()); - let content = if links.is_empty() { - div().w_full().child(styled_text) - } else { - // if link in block, interactive text is needed - // to handle link clicks - let (link_ranges, srcs): (Vec<_>, Vec<_>) = links.iter().cloned().unzip(); + let content = if links.is_empty() { + div().w_full().child(styled_text) + } else { + // if link in block, interactive text is needed + // to handle link clicks + let (link_ranges, srcs): (Vec<_>, Vec<_>) = links.iter().cloned().unzip(); - let weak = cx.entity(); - let t = gpui::InteractiveText::new(i, styled_text).on_click( - link_ranges, - move |i, _, cx| { - if let Some(src) = srcs.get(i) { - weak.update(cx, |this, cx| { - this.on_open_link(src, cx); - cx.notify(); - }) - } - }, - ); + let weak = cx.entity(); + let t = gpui::InteractiveText::new(i, styled_text).on_click( + link_ranges, + move |i, _, cx| { + if let Some(src) = srcs.get(i) { + weak.update(cx, |this, cx| { + this.on_open_link(src, cx); + cx.notify(); + }) + } + }, + ); - div().w_full().child(t) - }; + div().w_full().child(t) + }; - let mut div = match decoration { - | Some(d) => div() - .w_full() - .flex() - .flex_row() - .gap_2() - .items_start() - .child(d.clone()) - .child(div().flex_1().min_w_0().child(content)), - | None => div().w_full().child(content), - }; + let mut div = match decoration { + | Some(d) => div() + .w_full() + .flex() + .flex_row() + .gap_2() + .items_start() + .child(d.clone()) + .child(div().flex_1().min_w_0().child(content)), + | None => div().w_full().child(content), + }; - div.style().refine(&style); + div.style().refine(&style); - div - } + div + } } }); diff --git a/src/main.rs b/src/main.rs index 11751a9..08fdb86 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,17 +73,17 @@ fn setup_application(cx: &mut gpui::App) { let start = resume_application_state(cx); match start { - Start::FromScratch => { + | Start::FromScratch => { let screen = setup_wizard::new(); _ = setup_wizard::open_window(screen, cx); } - Start::FromSetup(state) => { + | Start::FromSetup(state) => { let screen = setup_wizard::from_saved(state); _ = setup_wizard::open_window(screen, cx); } - Start::FromSaved(_) => { + | Start::FromSaved(_) => { _ = dashboard::open_window(cx); } }; @@ -123,8 +123,8 @@ fn resume_application_state(cx: &mut gpui::App) -> Start { println!("[main] setup status: {:?}", setup_status); match setup_status { - setup_wizard::SetupStatus::NotStarted => Start::FromScratch, - setup_wizard::SetupStatus::InProgress(state) => Start::FromSetup(state), - setup_wizard::SetupStatus::Completed => Start::FromSaved(state), + | setup_wizard::SetupStatus::NotStarted => Start::FromScratch, + | setup_wizard::SetupStatus::InProgress(state) => Start::FromSetup(state), + | setup_wizard::SetupStatus::Completed => Start::FromSaved(state), } } diff --git a/src/query.rs b/src/query.rs index 5abfbd3..9e8c631 100644 --- a/src/query.rs +++ b/src/query.rs @@ -137,10 +137,10 @@ where })?; match wait_state { - WaitState::Cached => { + | WaitState::Cached => { return Ok(ent); } - WaitState::Waiting { rx, sub } => { + | WaitState::Waiting { rx, sub } => { _ = sub; _ = rx.await; } @@ -181,9 +181,9 @@ where 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()), + | 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()), } } @@ -284,11 +284,11 @@ where entity.raw.update(cx, |state, cx| { state.data = match result { - Ok(data) => { + | Ok(data) => { println!("[query] OK {}", q.key()); QueryData::Some(Box::new(data)) } - Err(err) => { + | Err(err) => { println!("[query] ERR {:?}: {:?}", q.key(), err); QueryData::Err(Box::new(err)) } @@ -317,8 +317,8 @@ where .raw .update(cx, |query, cx| { query.data = match result { - Ok(data) => QueryData::Some(Box::new(data)), - Err(err) => QueryData::Err(Box::new(err)), + | Ok(data) => QueryData::Some(Box::new(data)), + | Err(err) => QueryData::Err(Box::new(err)), }; cx.notify(); true diff --git a/src/screen/dashboard/issue_list.rs b/src/screen/dashboard/issue_list.rs index b57c661..0065f84 100644 --- a/src/screen/dashboard/issue_list.rs +++ b/src/screen/dashboard/issue_list.rs @@ -9,7 +9,7 @@ use crate::{ api::{self}, app, component::{ - font_icon::{FontIcon, font_icon}, + font_icon::{FontIcon, FontIconSvg, font_icon}, text::text, }, query::{self, QueryStatus, read_query, use_query}, @@ -134,6 +134,19 @@ impl gpui::RenderOnce for IssueListItem { fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement { let theme = app::current_theme(cx); + fn pill(label: impl gpui::IntoElement + gpui::Styled, icon: FontIconSvg) -> gpui::Div { + div() + .flex() + .flex_row() + .items_center() + .justify_start() + .rounded_full() + .px_2() + .gap_1() + .child(icon.size_3()) + .child(label.text_xs()) + } + let repo_name_text = match self.repo_name { | Some(name) => text(name), | None => text("Unknown repo"), @@ -141,39 +154,43 @@ impl gpui::RenderOnce for IssueListItem { .text_xs() .opacity(0.5); - let icon = if self.is_draft { - font_icon(FontIcon::PullRequestDraft) - .text_color(theme.colors.text) - .opacity(0.5) + let status_pill = if self.is_draft { + pill( + text("Draft").text_color(theme.colors.text), + font_icon(FontIcon::PullRequestDraft).text_color(theme.colors.text), + ) + .bg(theme.colors.surface) } else { match self.status { - | api::issues::PullRequestState::Closed => { - font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.danger) + | api::issues::PullRequestState::Closed => pill( + text("Closed").text_color(theme.colors.danger_on_solid), + font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.danger_on_solid), + ) + .bg(theme.colors.danger_solid), + | api::issues::PullRequestState::Merged => pill( + text("Merged").text_color(theme.colors.accent_on_solid), + font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.accent_on_solid), + ) + .bg(theme.colors.accent_solid), + | _ => pill( + text("Open").text_color(theme.colors.success_on_solid), + font_icon(FontIcon::PullRequestArrow).text_color(theme.colors.success_on_solid), + ) + .bg(theme.colors.success_solid), } - | api::issues::PullRequestState::Merged => { - font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.success) - } - | _ => font_icon(FontIcon::PullRequestArrow).text_color(theme.colors.success), - } - } - .flex_shrink_0() - .size_4(); - - let description_text = match self.description { - | Some(description) => text(description).text_xs(), - | None => text("No description provided").opacity(0.5).text_xs(), }; + let pills_row = div().flex().flex_row().gap_1().my_1().child(status_pill); + div() .relative() .w_full() - .px_1p5() + .px_3() .py_1() .gap_2() .flex() .flex_row() .items_center() - .child(icon) .child( div() .flex_1() @@ -191,33 +208,30 @@ impl gpui::RenderOnce for IssueListItem { .min_w_0() .line_clamp(2), ) - .child(description_text), + .child(pills_row), ) .when(!self.is_last, |it| { it.border_b_1().border_color(theme.colors.border) }) .when(self.is_selected, |it| { - it.bg(gpui::Rgba { - a: 0.05, - ..theme.colors.accent - }) - .overflow_hidden() - .border_r_1() - .child( - div() - .absolute() - .right_0() - .top_0() - .bottom_0() - .w_px() - .bg(theme.colors.accent) - .shadow(vec![gpui::BoxShadow { - blur_radius: px(16.), - spread_radius: px(2.), - color: gpui::Hsla::from(theme.colors.accent).alpha(0.8), - offset: point(px(-2.), px(0.)), - }]), - ) + it.bg(theme.colors.selection_bg) + .overflow_hidden() + .border_r_1() + .child( + div() + .absolute() + .right_0() + .top_0() + .bottom_0() + .w_px() + .bg(theme.colors.selection_border) + .shadow(vec![gpui::BoxShadow { + blur_radius: px(16.), + spread_radius: px(2.), + color: gpui::Hsla::from(theme.colors.selection_border).alpha(0.8), + offset: point(px(-2.), px(0.)), + }]), + ) }) } } diff --git a/src/screen/dashboard/pull_request_view.rs b/src/screen/dashboard/pull_request_view.rs index 166e604..5a67074 100644 --- a/src/screen/dashboard/pull_request_view.rs +++ b/src/screen/dashboard/pull_request_view.rs @@ -1,6 +1,6 @@ use gpui::{ AppContext, InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled, - div, img, point, prelude::FluentBuilder, px, + div, img, prelude::FluentBuilder, }; use crate::{ @@ -24,7 +24,7 @@ pub(crate) struct PullRequestView { #[derive(gpui::IntoElement)] struct Toolbar {} -pub fn new(cx: &mut gpui::Context) -> PullRequestView { +pub fn new(_cx: &mut gpui::Context) -> PullRequestView { PullRequestView { markdown_viewer: None, pull_request_query: None, @@ -89,37 +89,41 @@ impl PullRequestView { .rounded_full(); match pr.state { - | api::issues::PullRequestState::Open => { - status_pill = status_pill - .bg(theme.colors.success) - .child( - font_icon(FontIcon::PullRequestArrow) - .size_3() - .text_color(theme.colors.accent_text), - ) - .child(text("Open").text_color(theme.colors.accent_text).text_xs()); - } - | api::issues::PullRequestState::Closed => { - status_pill = status_pill - .bg(theme.colors.danger) - .child( - font_icon(FontIcon::PullRequestClosed) - .size_3() - .text_color(theme.colors.accent_text), - ) - .child( - text("Closed") - .text_color(theme.colors.accent_text) + | api::issues::PullRequestState::Open => { + status_pill = status_pill + .bg(theme.colors.success_solid) + .child( + font_icon(FontIcon::PullRequestArrow) + .size_3() + .text_color(theme.colors.success_on_solid), + ) + .child( + text("Open") + .text_color(theme.colors.success_on_solid) + .text_xs(), + ); + } + | api::issues::PullRequestState::Closed => { + status_pill = status_pill + .bg(theme.colors.danger_solid) + .child( + font_icon(FontIcon::PullRequestClosed) + .size_3() + .text_color(theme.colors.danger_on_solid), + ) + .child( + text("Closed") + .text_color(theme.colors.danger_on_solid) + .text_xs(), + ); + } + | api::issues::PullRequestState::Merged => { + status_pill = status_pill.bg(theme.colors.accent_solid).child( + text("Merged") + .text_color(theme.colors.accent_on_solid) .text_xs(), ); - } - | api::issues::PullRequestState::Merged => { - status_pill = status_pill.bg(theme.colors.accent).child( - text("Merged") - .text_color(theme.colors.accent_text) - .text_xs(), - ); - } + } } let merge_text = match ( @@ -127,48 +131,48 @@ impl PullRequestView { pr.base_branch_name.as_ref(), pr.head_branch_name.as_ref(), ) { - | (Some(author), Some(base_branch), Some(head_branch)) => { - let str = format!( - "{} requested to merge {} into {}", - author.login, head_branch, base_branch - ); + | (Some(author), Some(base_branch), Some(head_branch)) => { + let str = format!( + "{} requested to merge {} into {}", + author.login, head_branch, base_branch + ); - let head_branch_text_offset = author.login.len() + 20; - let base_branch_text_offset = head_branch_text_offset + head_branch.len() + 6; + let head_branch_text_offset = author.login.len() + 20; + let base_branch_text_offset = head_branch_text_offset + head_branch.len() + 6; - let highlights = [ - ( - 0..author.login.len(), - gpui::HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - ), - ( - head_branch_text_offset..head_branch_text_offset + head_branch.len(), - gpui::HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - color: Some(theme.colors.accent.into()), - ..Default::default() - }, - ), - ( - base_branch_text_offset..base_branch_text_offset + base_branch.len(), - gpui::HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - color: Some(theme.colors.accent.into()), - ..Default::default() - }, - ), - ]; + let highlights = [ + ( + 0..author.login.len(), + gpui::HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + ..Default::default() + }, + ), + ( + head_branch_text_offset..head_branch_text_offset + head_branch.len(), + gpui::HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + color: Some(theme.colors.accent_fg.into()), + ..Default::default() + }, + ), + ( + base_branch_text_offset..base_branch_text_offset + base_branch.len(), + gpui::HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + color: Some(theme.colors.accent_fg.into()), + ..Default::default() + }, + ), + ]; - Some(( - author, - gpui::StyledText::new(str).with_highlights(highlights), - )) - } + Some(( + author, + gpui::StyledText::new(str).with_highlights(highlights), + )) + } - | _ => None, + | _ => None, }; let metadata_line = @@ -257,24 +261,24 @@ impl gpui::Render for PullRequestView { cx: &mut gpui::Context, ) -> impl gpui::IntoElement { div().size_full().child(match &self.pull_request_query { - | Some(q) => match read_query(q, cx) { - | QueryStatus::Loaded(pr) => self.pr_content(pr, cx), - | QueryStatus::Err(e) => div() - .size_full() - .child(format!("{:?}", e)) - .into_any_element(), - | QueryStatus::Loading => div() - .size_full() - .child("loading pr content") - .into_any_element(), - }, - | None => div().size_full().child("no pr selected").into_any_element(), + | Some(q) => match read_query(q, cx) { + | QueryStatus::Loaded(pr) => self.pr_content(pr, cx), + | QueryStatus::Err(e) => div() + .size_full() + .child(format!("{:?}", e)) + .into_any_element(), + | QueryStatus::Loading => div() + .size_full() + .child("loading pr content") + .into_any_element(), + }, + | None => div().size_full().child("no pr selected").into_any_element(), }) } } impl gpui::RenderOnce for Toolbar { - fn render(self, window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement { + fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement { fn toolbar_button(id: impl Into) -> Button { button(id) .px_2p5() diff --git a/src/screen/dashboard/screen.rs b/src/screen/dashboard/screen.rs index 5903522..fce2fd2 100644 --- a/src/screen/dashboard/screen.rs +++ b/src/screen/dashboard/screen.rs @@ -5,7 +5,6 @@ use crate::{ screen::dashboard::{ issue_list::{self, IssueList}, pull_request_view::{self, PullRequestView}, - sidebar::{self, Sidebar, SidebarItemValue}, titlebar::{self, TitleBar}, }, }; @@ -13,7 +12,6 @@ use crate::{ pub(crate) struct Screen { titlebar: gpui::Entity, issue_list: gpui::Entity, - sidebar: gpui::Entity, pull_request_view: gpui::Entity, issue_filter: Option<&'static str>, @@ -23,7 +21,6 @@ pub(crate) fn new(cx: &mut gpui::Context) -> Screen { let mut screen = Screen { titlebar: cx.new(titlebar::new), issue_list: cx.new(issue_list::new), - sidebar: cx.new(|_| sidebar::new()), pull_request_view: cx.new(pull_request_view::new), issue_filter: None, @@ -34,35 +31,15 @@ pub(crate) fn new(cx: &mut gpui::Context) -> Screen { impl Screen { fn on_create(&mut self, cx: &mut gpui::Context) { - let on_item_change = cx.listener(|this, value, _, cx| { - this.handle_sidebar_item_change(value, cx); - }); - self.sidebar.update(cx, |sidebar, _| { - sidebar.on_item_change(on_item_change); - }); - _ = cx .subscribe(&self.issue_list, |this, _, event, cx| match event { - | issue_list::Event::ItemSelected(pr_id) => { - this.handle_issue_list_item_selected(pr_id, cx); - } + | issue_list::Event::ItemSelected(pr_id) => { + this.handle_issue_list_item_selected(pr_id, cx); + } }) .detach(); } - fn handle_sidebar_item_change( - &mut self, - value: &SidebarItemValue, - cx: &mut gpui::Context, - ) { - match value { - | SidebarItemValue::PullRequest { filter } => { - self.issue_filter = Some(*filter); - cx.notify(); - } - } - } - fn handle_issue_list_item_selected( &mut self, id: &api::issues::Id, @@ -97,21 +74,12 @@ impl gpui::Render for Screen { .flex_1() .min_h_0() .w_full() - .child( - div() - .w_40() - .flex_shrink_0() - .h_full() - .child(self.sidebar.clone()), - ) .child( div() .w_64() .flex_shrink_0() .h_full() - .bg(theme.colors.surface) - .border_x_1() - .border_color(theme.colors.border) + .bg(theme.colors.surface_chrome) .overflow_hidden() .child(self.issue_list.clone()), ) diff --git a/src/screen/dashboard/sidebar.rs b/src/screen/dashboard/sidebar.rs index 02581a1..251ae95 100644 --- a/src/screen/dashboard/sidebar.rs +++ b/src/screen/dashboard/sidebar.rs @@ -160,19 +160,19 @@ impl gpui::RenderOnce for SidebarItem { .px_2() .py_1() .gap_2() - .child(font_icon(self.icon).size_3().when(self.is_selected, |it| { - it.text_color(theme.colors.accent_text) - })) + .child( + font_icon(self.icon) + .size_3() + .when(self.is_selected, |it| it.text_color(theme.colors.accent_fg)), + ) .child( text(self.title) .text_sm() .leading_tight() - .when(self.is_selected, |it| { - it.text_color(theme.colors.accent_text) - }), + .when(self.is_selected, |it| it.text_color(theme.colors.accent_fg)), ), ) .when_some(self.on_click, |it, f| it.on_click(f)) - .when(self.is_selected, |it| it.bg(theme.colors.accent)) + .when(self.is_selected, |it| it.bg(theme.colors.selection_bg)) } } diff --git a/src/screen/dashboard/titlebar.rs b/src/screen/dashboard/titlebar.rs index 0f9c013..e4975a5 100644 --- a/src/screen/dashboard/titlebar.rs +++ b/src/screen/dashboard/titlebar.rs @@ -1,7 +1,7 @@ -use gpui::{ParentElement, Styled, TitlebarOptions, div}; +use gpui::{ParentElement, Styled, div}; use crate::component::button::button; -use crate::query::{self, QueryStatus, read_query, use_lazy_query, use_query}; +use crate::query::{self, QueryStatus, read_query, use_lazy_query}; use crate::{ api, app, component::{ @@ -32,13 +32,13 @@ impl gpui::Render for TitleBar { let user = read_query(&self.fetch_user_query, cx); let user_avatar = match user { - QueryStatus::Err(api::Error::Unauthenticated) => div().absolute().right_2p5().child( + | QueryStatus::Err(api::Error::Unauthenticated) => div().absolute().right_2p5().child( button("login-btn") .leading(font_icon(FontIcon::Github)) .label("Login"), ), - _ => div(), + | _ => div(), }; div() @@ -50,7 +50,7 @@ impl gpui::Render for TitleBar { .flex() .px(g.safe_area.size.width) .py_2() - .bg(g.current_theme.colors.background) + .bg(g.current_theme.colors.surface_chrome) .text_color(g.current_theme.colors.text) .relative() .border_b_1() @@ -61,7 +61,7 @@ impl gpui::Render for TitleBar { } impl RepoSelector { - pub fn new(cx: &mut gpui::Context) -> Self { + pub fn new(_cx: &mut gpui::Context) -> Self { Self {} } } diff --git a/src/screen/setup_wizard/github_step.rs b/src/screen/setup_wizard/github_step.rs index f16f7a4..8dee29d 100644 --- a/src/screen/setup_wizard/github_step.rs +++ b/src/screen/setup_wizard/github_step.rs @@ -189,7 +189,7 @@ impl GithubStepView { let poll_interval = u64::from(*interval); match read_query(query, cx) { - QueryStatus::Loaded(data) => { + | QueryStatus::Loaded(data) => { let auth_tokens = api::AuthTokens { access_token: data.access_token.clone(), }; @@ -226,7 +226,7 @@ impl GithubStepView { .detach(); } - QueryStatus::Err(api::Error::Github(api::GithubError { error, .. })) => { + | QueryStatus::Err(api::Error::Github(api::GithubError { error, .. })) => { if error == "authorization_pending" { cx.spawn(async move |weak, cx| { Timer::after(Duration::from_secs(poll_interval)).await; @@ -242,7 +242,7 @@ impl GithubStepView { } } - _ => {} + | _ => {} } } @@ -263,8 +263,8 @@ impl GithubStepView { let theme = app::current_theme(cx); 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), + | 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(); @@ -358,14 +358,14 @@ impl gpui::Render for GithubStepView { cx: &mut gpui::Context, ) -> impl gpui::IntoElement { let (can_go_next, header, body) = match self.user_query { - None => (false, self.header(), self.device_code_area(cx)), - Some(ref q) => { + | None => (false, self.header(), self.device_code_area(cx)), + | Some(ref q) => { let user_query = read_query(q, cx); match user_query { - QueryStatus::Loaded(user) => { + | QueryStatus::Loaded(user) => { (true, connected_header(), connected_body(user, cx)) } - _ => (false, self.header(), self.device_code_area(cx)), + | _ => (false, self.header(), self.device_code_area(cx)), } } }; @@ -436,7 +436,7 @@ fn connected_body(user: &api::user::User, cx: &gpui::Context) -> .rounded_2xl() .border_1() .w_full() - .border_color(theme.colors.surface_elevated) + .border_color(theme.colors.border) .p_4() .child( div() @@ -461,9 +461,13 @@ fn connected_body(user: &api::user::User, cx: &gpui::Context) -> .child( div() .rounded_full() - .bg(theme.colors.accent) + .bg(theme.colors.success_solid) .p_1() - .child(font_icon(FontIcon::Check).size_4()), + .child( + font_icon(FontIcon::Check) + .size_4() + .text_color(theme.colors.success_on_solid), + ), ), ) } diff --git a/src/screen/setup_wizard/mod.rs b/src/screen/setup_wizard/mod.rs index a2c3353..d1f0ee1 100644 --- a/src/screen/setup_wizard/mod.rs +++ b/src/screen/setup_wizard/mod.rs @@ -51,9 +51,9 @@ pub fn open_window(screen: Screen, cx: &mut gpui::App) -> anyhow::Result<()> { impl Step { pub const fn order(&self) -> usize { match self { - Step::Welcome => 0, - Step::ConnectToGithub => 1, - Step::SetupComplete => 2, + | Step::Welcome => 0, + | Step::ConnectToGithub => 1, + | Step::SetupComplete => 2, } } } diff --git a/src/screen/setup_wizard/screen.rs b/src/screen/setup_wizard/screen.rs index ca4a655..229af62 100644 --- a/src/screen/setup_wizard/screen.rs +++ b/src/screen/setup_wizard/screen.rs @@ -38,9 +38,9 @@ pub(crate) fn from_saved(state: StoredSetupState) -> Screen { impl Screen { fn advance_to_next_step(&mut self, cx: &mut gpui::Context) { let next_step = match self.current_step { - Step::Welcome => Step::ConnectToGithub, - Step::ConnectToGithub => Step::SetupComplete, - _ => panic!(), + | Step::Welcome => Step::ConnectToGithub, + | Step::ConnectToGithub => Step::SetupComplete, + | _ => panic!(), }; self.current_step = next_step; cx.notify(); @@ -67,8 +67,8 @@ impl Screen { cx: &mut gpui::Context, ) -> &gpui::Entity { match self.github_step_view { - Some(ref v) => v, - None => { + | Some(ref v) => v, + | None => { let weak = cx.weak_entity(); self.github_step_view = Some(cx.new(|cx| { let mut v = github_step::new(cx); @@ -90,9 +90,9 @@ impl Screen { .enumerate() .map(|(i, step)| { let label = match step { - Step::Welcome => "Welcome!", - Step::ConnectToGithub => "Connect to GitHub", - Step::SetupComplete => "Complete!", + | Step::Welcome => "Welcome!", + | Step::ConnectToGithub => "Connect to GitHub", + | Step::SetupComplete => "Complete!", }; let is_completed = i < self.current_step.order(); let is_current = self.current_step == *step; @@ -138,16 +138,16 @@ impl gpui::Render for Screen { cx: &mut gpui::Context, ) -> impl gpui::IntoElement { let step_view = match self.current_step { - Step::Welcome => welcome_step() + | Step::Welcome => welcome_step() .on_next(cx.listener(|this, _, _, cx| this.advance_to_next_step(cx))) .into_any_element(), - Step::ConnectToGithub => match self.github_step_view { - Some(ref view) => view.clone().into_any_element(), - None => self.init_github_step_view(cx).clone().into_any_element(), + | Step::ConnectToGithub => match self.github_step_view { + | Some(ref view) => view.clone().into_any_element(), + | None => self.init_github_step_view(cx).clone().into_any_element(), }, - Step::SetupComplete => setup_complete_step().into_any_element(), + | Step::SetupComplete => setup_complete_step().into_any_element(), }; let theme = app::current_theme(cx); @@ -165,7 +165,7 @@ impl gpui::Render for Screen { .justify_center() .w_1_3() .h_full() - .bg(theme.colors.surface) + .bg(theme.colors.surface_chrome) .relative() .child( div() diff --git a/src/storage.rs b/src/storage.rs index b90d834..83b7c5d 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -16,12 +16,12 @@ pub(crate) struct PersistedState { pub(crate) fn data_dir_path() -> std::path::PathBuf { match std::env::consts::OS { - "macos" => std::env::home_dir() + | "macos" => std::env::home_dir() .unwrap() .join("Library") .join("Application Support") .join("novem"), - _ => unimplemented!( + | _ => unimplemented!( "data_dir_path is unimplemented for OS: {}", std::env::consts::OS ), diff --git a/src/theme.rs b/src/theme.rs index 66724ef..1e7eb32 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,6 +1,6 @@ -use gpui::Rgba; +mod catppuccin; -use crate::colors::hex; +use gpui::Rgba; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ThemeMode { @@ -28,15 +28,49 @@ pub struct ThemeColors { pub background: Rgba, pub surface: Rgba, pub surface_elevated: Rgba, + pub surface_chrome: Rgba, + pub surface_hover: Rgba, + pub surface_active: Rgba, pub border: Rgba, + pub border_muted: Rgba, + pub border_strong: Rgba, + pub focus_ring: Rgba, pub text: Rgba, pub text_muted: Rgba, - pub accent: Rgba, - pub accent_hover: Rgba, - pub accent_text: Rgba, - pub success: Rgba, - pub warning: Rgba, - pub danger: Rgba, + pub text_subtle: Rgba, + pub text_disabled: Rgba, + pub icon_muted: Rgba, + pub link: Rgba, + pub link_hover: Rgba, + pub code_bg: Rgba, + pub code_border: Rgba, + pub selection_bg: Rgba, + pub selection_border: Rgba, + pub accent_fg: Rgba, + pub accent_muted: Rgba, + pub accent_border: Rgba, + pub accent_solid: Rgba, + pub accent_on_solid: Rgba, + pub success_fg: Rgba, + pub success_muted: Rgba, + pub success_border: Rgba, + pub success_solid: Rgba, + pub success_on_solid: Rgba, + pub warning_fg: Rgba, + pub warning_muted: Rgba, + pub warning_border: Rgba, + pub warning_solid: Rgba, + pub warning_on_solid: Rgba, + pub danger_fg: Rgba, + pub danger_muted: Rgba, + pub danger_border: Rgba, + pub danger_solid: Rgba, + pub danger_on_solid: Rgba, + pub info_fg: Rgba, + pub info_muted: Rgba, + pub info_border: Rgba, + pub info_solid: Rgba, + pub info_on_solid: Rgba, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -52,20 +86,20 @@ impl ThemeFamily { pub const fn id(self) -> &'static str { match self { - | Self::Catppuccin => "catppuccin", + | Self::Catppuccin => catppuccin::FAMILY_ID, } } pub const fn label(self) -> &'static str { match self { - | Self::Catppuccin => "Catppuccin", + | Self::Catppuccin => catppuccin::FAMILY_LABEL, } } pub const fn variant(self, mode: ThemeMode) -> ThemeVariant { match (self, mode) { - | (Self::Catppuccin, ThemeMode::Light) => ThemeVariant::CatppuccinLatte, - | (Self::Catppuccin, ThemeMode::Dark) => ThemeVariant::CatppuccinMocha, + | (Self::Catppuccin, ThemeMode::Light) => ThemeVariant::CatppuccinLatte, + | (Self::Catppuccin, ThemeMode::Dark) => ThemeVariant::CatppuccinMocha, } } @@ -92,64 +126,28 @@ impl ThemeVariant { pub const fn family(self) -> ThemeFamily { match self { - | Self::CatppuccinLatte | Self::CatppuccinMocha => ThemeFamily::Catppuccin, + | Self::CatppuccinLatte | Self::CatppuccinMocha => ThemeFamily::Catppuccin, } } pub const fn mode(self) -> ThemeMode { match self { - | Self::CatppuccinLatte => ThemeMode::Light, - | Self::CatppuccinMocha => ThemeMode::Dark, + | Self::CatppuccinLatte => ThemeMode::Light, + | Self::CatppuccinMocha => ThemeMode::Dark, } } pub const fn label(self) -> &'static str { match self { - | Self::CatppuccinLatte => "Catppuccin Latte", - | Self::CatppuccinMocha => "Catppuccin Mocha", + | Self::CatppuccinLatte => catppuccin::LATTE_LABEL, + | Self::CatppuccinMocha => catppuccin::MOCHA_LABEL, } } pub const fn theme(self) -> Theme { match self { - | Self::CatppuccinLatte => Theme { - id: "catppuccin-latte", - name: "Catppuccin Latte", - mode: ThemeMode::Light, - colors: ThemeColors { - background: hex(0xeff1f5), - surface: hex(0xeff1f5), - surface_elevated: hex(0xdce0e8), - border: hex(0xccd0da), - text: hex(0x4c4f69), - text_muted: hex(0x6c6f85), - accent: hex(0x8839ef), - accent_hover: hex(0x7287fd), - accent_text: hex(0xeff1f5), - success: hex(0x40a02b), - warning: hex(0xdf8e1d), - danger: hex(0xd20f39), - }, - }, - | Self::CatppuccinMocha => Theme { - id: "catppuccin-mocha", - name: "Catppuccin Mocha", - mode: ThemeMode::Dark, - colors: ThemeColors { - background: hex(0x1e1e2e), - surface: hex(0x181825), - surface_elevated: hex(0x313244), - border: hex(0x45475a), - text: hex(0xcdd6f4), - text_muted: hex(0xa6adc8), - accent: hex(0xcba6f7), - accent_hover: hex(0xb4befe), - accent_text: hex(0x1e1e2e), - success: hex(0xa6e3a1), - warning: hex(0xf9e2af), - danger: hex(0xf38ba8), - }, - }, + | Self::CatppuccinLatte => catppuccin::latte(), + | Self::CatppuccinMocha => catppuccin::mocha(), } } } @@ -157,8 +155,10 @@ impl ThemeVariant { impl From for ThemeMode { fn from(value: gpui::WindowAppearance) -> Self { match value { - | gpui::WindowAppearance::Light | gpui::WindowAppearance::VibrantLight => ThemeMode::Light, - | gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => ThemeMode::Dark, + | gpui::WindowAppearance::Light | gpui::WindowAppearance::VibrantLight => { + ThemeMode::Light + } + | gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => ThemeMode::Dark, } } } diff --git a/src/theme/catppuccin.rs b/src/theme/catppuccin.rs new file mode 100644 index 0000000..fe471e6 --- /dev/null +++ b/src/theme/catppuccin.rs @@ -0,0 +1,121 @@ +use crate::colors::{hex, hex_alpha}; + +use super::{Theme, ThemeColors, ThemeMode}; + +pub(crate) const FAMILY_ID: &str = "catppuccin"; +pub(crate) const FAMILY_LABEL: &str = "Catppuccin"; + +pub(crate) const LATTE_LABEL: &str = "Catppuccin Latte"; +pub(crate) const MOCHA_LABEL: &str = "Catppuccin Mocha"; + +pub(crate) const fn latte() -> Theme { + Theme { + id: "catppuccin-latte", + name: LATTE_LABEL, + mode: ThemeMode::Light, + colors: ThemeColors { + background: hex(0xeff1f5), + surface: hex(0xeff1f5), + surface_elevated: hex(0xdce0e8), + surface_chrome: hex(0xe6e9ef), + surface_hover: hex(0xe6e9ef), + surface_active: hex(0xdce0e8), + border: hex(0xbcc0cc), + border_muted: hex(0xccd0da), + border_strong: hex(0xacb0be), + focus_ring: hex(0x7287fd), + text: hex(0x4c4f69), + text_muted: hex(0x5c5f77), + text_subtle: hex(0x6c6f85), + text_disabled: hex(0x9ca0b0), + icon_muted: hex(0x6c6f85), + link: hex(0x1e66f5), + link_hover: hex(0x7287fd), + code_bg: hex(0xe6e9ef), + code_border: hex(0xccd0da), + selection_bg: hex_alpha(0x8839ef, 0.10), + selection_border: hex_alpha(0x8839ef, 0.35), + accent_fg: hex(0x8839ef), + accent_muted: hex_alpha(0x8839ef, 0.12), + accent_border: hex_alpha(0x8839ef, 0.28), + accent_solid: hex(0x8839ef), + accent_on_solid: hex(0xeff1f5), + success_fg: hex(0x40a02b), + success_muted: hex_alpha(0x40a02b, 0.12), + success_border: hex_alpha(0x40a02b, 0.28), + success_solid: hex(0x40a02b), + success_on_solid: hex(0x1e1e2e), + warning_fg: hex(0xdf8e1d), + warning_muted: hex_alpha(0xdf8e1d, 0.12), + warning_border: hex_alpha(0xdf8e1d, 0.32), + warning_solid: hex(0xdf8e1d), + warning_on_solid: hex(0x1e1e2e), + danger_fg: hex(0xd20f39), + danger_muted: hex_alpha(0xd20f39, 0.12), + danger_border: hex_alpha(0xd20f39, 0.28), + danger_solid: hex(0xd20f39), + danger_on_solid: hex(0xeff1f5), + info_fg: hex(0x1e66f5), + info_muted: hex_alpha(0x1e66f5, 0.12), + info_border: hex_alpha(0x1e66f5, 0.28), + info_solid: hex(0x1e66f5), + info_on_solid: hex(0xeff1f5), + }, + } +} + +pub(crate) const fn mocha() -> Theme { + Theme { + id: "catppuccin-mocha", + name: MOCHA_LABEL, + mode: ThemeMode::Dark, + colors: ThemeColors { + background: hex(0x1e1e2e), + surface: hex(0x181825), + surface_elevated: hex(0x313244), + surface_chrome: hex(0x11111b), + surface_hover: hex(0x313244), + surface_active: hex(0x45475a), + border: hex(0x585b70), + border_muted: hex(0x45475a), + border_strong: hex(0x6c7086), + focus_ring: hex(0xb4befe), + text: hex(0xcdd6f4), + text_muted: hex(0xbac2de), + text_subtle: hex(0xa6adc8), + text_disabled: hex(0x7f849c), + icon_muted: hex(0xa6adc8), + link: hex(0x89b4fa), + link_hover: hex(0xb4befe), + code_bg: hex(0x11111b), + code_border: hex(0x45475a), + selection_bg: hex_alpha(0xcba6f7, 0.18), + selection_border: hex_alpha(0xcba6f7, 0.45), + accent_fg: hex(0xcba6f7), + accent_muted: hex_alpha(0xcba6f7, 0.18), + accent_border: hex_alpha(0xcba6f7, 0.34), + accent_solid: hex(0xcba6f7), + accent_on_solid: hex(0x1e1e2e), + success_fg: hex(0xa6e3a1), + success_muted: hex_alpha(0xa6e3a1, 0.18), + success_border: hex_alpha(0xa6e3a1, 0.34), + success_solid: hex(0xa6e3a1), + success_on_solid: hex(0x1e1e2e), + warning_fg: hex(0xf9e2af), + warning_muted: hex_alpha(0xf9e2af, 0.18), + warning_border: hex_alpha(0xf9e2af, 0.38), + warning_solid: hex(0xf9e2af), + warning_on_solid: hex(0x1e1e2e), + danger_fg: hex(0xf38ba8), + danger_muted: hex_alpha(0xf38ba8, 0.18), + danger_border: hex_alpha(0xf38ba8, 0.34), + danger_solid: hex(0xf38ba8), + danger_on_solid: hex(0x1e1e2e), + info_fg: hex(0x89b4fa), + info_muted: hex_alpha(0x89b4fa, 0.18), + info_border: hex_alpha(0x89b4fa, 0.34), + info_solid: hex(0x89b4fa), + info_on_solid: hex(0x1e1e2e), + }, + } +}